/**
* Copyright 2013, Big Switch Networks, Inc.
*
* 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.staticentry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import net.floodlightcontroller.core.FloodlightContext;
import net.floodlightcontroller.core.HAListenerTypeMarker;
import net.floodlightcontroller.core.IFloodlightProviderService;
import net.floodlightcontroller.core.IHAListener;
import net.floodlightcontroller.core.IOFMessageListener;
import net.floodlightcontroller.core.IOFSwitch;
import net.floodlightcontroller.core.IOFSwitchListener;
import net.floodlightcontroller.core.PortChangeType;
import net.floodlightcontroller.core.internal.IOFSwitchService;
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.staticentry.web.StaticEntryWebRoutable;
import net.floodlightcontroller.staticentry.web.StaticFlowEntryWebRoutable;
import net.floodlightcontroller.staticentry.web.StaticFlowWebRoutable;
import net.floodlightcontroller.storage.IResultSet;
import net.floodlightcontroller.storage.IStorageSourceListener;
import net.floodlightcontroller.storage.IStorageSourceService;
import net.floodlightcontroller.storage.StorageException;
import net.floodlightcontroller.util.ActionUtils;
import net.floodlightcontroller.util.FlowModUtils;
import net.floodlightcontroller.util.GroupUtils;
import net.floodlightcontroller.util.InstructionUtils;
import net.floodlightcontroller.util.MatchUtils;
import org.projectfloodlight.openflow.protocol.OFFactories;
import org.projectfloodlight.openflow.protocol.OFFlowAdd;
import org.projectfloodlight.openflow.protocol.OFFlowMod;
import org.projectfloodlight.openflow.protocol.OFFlowRemoved;
import org.projectfloodlight.openflow.protocol.OFFlowRemovedReason;
import org.projectfloodlight.openflow.protocol.OFGroupMod;
import org.projectfloodlight.openflow.protocol.OFInstructionType;
import org.projectfloodlight.openflow.protocol.OFPortDesc;
import org.projectfloodlight.openflow.protocol.OFMessage;
import org.projectfloodlight.openflow.protocol.OFType;
import org.projectfloodlight.openflow.protocol.OFVersion;
import org.projectfloodlight.openflow.protocol.match.MatchFields;
import org.projectfloodlight.openflow.types.DatapathId;
import org.projectfloodlight.openflow.types.TableId;
import org.projectfloodlight.openflow.types.U32;
import org.projectfloodlight.openflow.types.U64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableSet;
/**
* This module is responsible for maintaining a set of static flows on
* switches. This is just a big 'ol dumb list of flows and groups and something external
* is responsible for ensuring they make sense for the network.
*/
public class StaticEntryPusher
implements IOFSwitchListener, IFloodlightModule, IStaticEntryPusherService, IStorageSourceListener, IOFMessageListener {
protected static Logger log = LoggerFactory.getLogger(StaticEntryPusher.class);
public static final String MODULE_NAME = "staticentrypusher";
public static final int STATIC_ENTRY_APP_ID = 10;
static {
AppCookie.registerApp(STATIC_ENTRY_APP_ID, MODULE_NAME);
}
public static final String TABLE_NAME = "controller_staticentrytable";
public static class Columns {
public static final String COLUMN_NAME = "name";
public static final String COLUMN_ENTRY_TYPE = "entry_type";
public static final String ENTRY_TYPE_FLOW = "flow";
public static final String ENTRY_TYPE_GROUP = "group";
public static final String COLUMN_SWITCH = "switch";
public static final String COLUMN_TABLE_ID = "table";
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";
/* NOTE: Use MatchUtil's names for MatchField column names */
/*
* Support for OF1.0 generic transport ports (possibly from the REST API).
* Only use these to read them in, but store them as the type of port their IpProto
*/
public static final String COLUMN_NW_TOS = MatchUtils.STR_NW_TOS;
public static final String COLUMN_TP_SRC = MatchUtils.STR_TP_SRC;
public static final String COLUMN_TP_DST = MatchUtils.STR_TP_DST;
public static final String COLUMN_ACTIONS = "actions";
/* NOTE: Use InstructionUtil's names for Instruction column names */
public static final String COLUMN_GROUP_TYPE = GroupUtils.GROUP_TYPE;
public static final String COLUMN_GROUP_BUCKETS = GroupUtils.GROUP_BUCKETS;
public static final String COLUMN_GROUP_ID = GroupUtils.GROUP_ID;
private static Set<String> ALL_COLUMNS; /* Use internally to query only */
}
protected IFloodlightProviderService floodlightProviderService;
protected IOFSwitchService switchService;
protected IStorageSourceService storageSourceService;
protected IRestApiService restApiService;
private IHAListener haListener;
// Map<DPID, Map<Name, OFMessage>>; OFMessage can be null to indicate non-active
protected Map<String, Map<String, OFMessage>> entriesFromStorage;
// Entry Name -> DPID of Switch it's on
protected Map<String, String> entry2dpid;
// 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) {
OFMessage m1 = entriesFromStorage.get(dpid).get(o1);
OFMessage m2 = entriesFromStorage.get(dpid).get(o2);
if (m1 == null || m2 == null) {// sort active=false flows by key
return o1.compareTo(o2);
}
if (m1 instanceof OFFlowMod && m2 instanceof OFFlowMod) {
return (int) (U32.of(((OFFlowMod) m1).getPriority()).getValue() - U32.of(((OFFlowMod) m2).getPriority()).getValue());
} else if (m1 instanceof OFFlowMod) {
return 1;
} else if (m2 instanceof OFFlowMod) {
return -1;
} else {
return 0;
}
}
};
public static String matchFieldToColumnName(MatchFields mf) {
return MatchUtils.getMatchFieldName(mf);
}
public static String intructionToColumnName(OFInstructionType t) {
return InstructionUtils.getInstructionName(t);
}
/**
* 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 floodlightProviderService;
}
public void setFloodlightProvider(IFloodlightProviderService floodlightProviderService) {
this.floodlightProviderService = floodlightProviderService;
}
public void setStorageSource(IStorageSourceService storageSourceService) {
this.storageSourceService = storageSourceService;
}
/**
* 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(DatapathId switchId) {
IOFSwitch sw = switchService.getSwitch(switchId);
if (sw == null)
return;
String stringId = sw.getId().toString();
if ((entriesFromStorage != null) && (entriesFromStorage.containsKey(stringId))) {
Map<String, OFMessage> entries = entriesFromStorage.get(stringId);
List<String> sortedList = new ArrayList<String>(entries.keySet());
// weird that Collections.sort() returns void
Collections.sort( sortedList, new FlowModSorter(stringId));
for (String entryName : sortedList) {
OFMessage message = entries.get(entryName);
if (message != null) {
if (log.isDebugEnabled()) {
log.debug("Pushing static entry {} for {}", stringId, entryName);
}
writeOFMessageToSwitch(sw.getId(), message);
}
}
}
}
/**
* Used only for bundle-local indexing
*
* @param map
* @return
*/
protected Map<String, String> computeEntry2DpidMap(
Map<String, Map<String, OFMessage>> map) {
Map<String, String> ret = new ConcurrentHashMap<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
*/
private Map<String, Map<String, OFMessage>> readEntriesFromStorage() {
Map<String, Map<String, OFMessage>> entries = new ConcurrentHashMap<String, Map<String, OFMessage>>();
try {
Map<String, Object> row;
// null1=no predicate, null2=no ordering
IResultSet resultSet = storageSourceService.executeQuery(TABLE_NAME, Columns.ALL_COLUMNS.toArray(new String[Columns.ALL_COLUMNS.size()]), 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 entry.
* If an entry is inactive, mark it with null
*
* @param row
* @param entries
*/
void parseRow(Map<String, Object> row, Map<String, Map<String, OFMessage>> entries) {
String switchName = null;
String entryName = null;
String entryType = Columns.ENTRY_TYPE_FLOW;
StringBuffer matchString = new StringBuffer();
OFFlowMod.Builder fmb = null;
OFGroupMod.Builder gmb = null;
if (!row.containsKey(Columns.COLUMN_SWITCH) || !row.containsKey(Columns.COLUMN_NAME)) {
log.warn("Skipping entry with missing required 'switch' or 'name' entry: {}", row);
return;
}
try {
switchName = DatapathId.of((String) row.get(Columns.COLUMN_SWITCH)).toString();
entryName = (String) row.get(Columns.COLUMN_NAME);
String tmp = (String) row.get(Columns.COLUMN_ENTRY_TYPE);
if (tmp != null) {
tmp = tmp.toLowerCase().trim();
if (tmp.equals(Columns.ENTRY_TYPE_GROUP)) {
entryType = Columns.ENTRY_TYPE_GROUP;
}
} /* else use default of flow */
if (!entries.containsKey(switchName)) {
entries.put(switchName, new ConcurrentHashMap<String, OFMessage>());
}
/* get the correct builder for the OF version supported by the switch */
try {
if (entryType.equals(Columns.ENTRY_TYPE_FLOW)) {
fmb = OFFactories.getFactory(switchService.getSwitch(DatapathId.of(switchName)).getOFFactory().getVersion()).buildFlowModify();
StaticEntries.initDefaultFlowMod(fmb, entryName);
} else if (entryType.equals(Columns.ENTRY_TYPE_GROUP)) {
gmb = OFFactories.getFactory(switchService.getSwitch(DatapathId.of(switchName)).getOFFactory().getVersion()).buildGroupModify();
} else {
log.error("Not adding a flow or a group? Possible Static Flow Pusher bug");
return;
}
} catch (NullPointerException e) {
/* switch was not connected/known */
storageSourceService.deleteRowAsync(TABLE_NAME, entryName);
log.error("Deleting entry {}. Switch {} was not connected to the controller, and we need to know the OF protocol version to compose the flow mod.", entryName, switchName);
return;
}
for (String key : row.keySet()) {
if (row.get(key) == null) {
continue;
}
if (key.equals(Columns.COLUMN_SWITCH) || key.equals(Columns.COLUMN_NAME)) {
continue; // already handled
}
if (key.equals(Columns.COLUMN_ACTIVE)) {
if (!Boolean.valueOf((String) row.get(Columns.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(Columns.COLUMN_HARD_TIMEOUT) && fmb != null) {
fmb.setHardTimeout(Integer.valueOf((String) row.get(Columns.COLUMN_HARD_TIMEOUT)));
} else if (key.equals(Columns.COLUMN_IDLE_TIMEOUT) && fmb != null) {
fmb.setIdleTimeout(Integer.valueOf((String) row.get(Columns.COLUMN_IDLE_TIMEOUT)));
} else if (key.equals(Columns.COLUMN_TABLE_ID) && fmb != null) {
if (fmb.getVersion().compareTo(OFVersion.OF_10) > 0) {
fmb.setTableId(TableId.of(Integer.parseInt((String) row.get(key)))); // support multiple flow tables for OF1.1+
} else {
log.error("Table not supported in OpenFlow 1.0");
}
} else if (key.equals(Columns.COLUMN_ACTIONS) && fmb != null) {
ActionUtils.fromString(fmb, (String) row.get(Columns.COLUMN_ACTIONS));
} else if (key.equals(Columns.COLUMN_COOKIE) && fmb != null) {
fmb.setCookie(StaticEntries.computeEntryCookie(Integer.valueOf((String) row.get(Columns.COLUMN_COOKIE)), entryName));
} else if (key.equals(Columns.COLUMN_PRIORITY) && fmb != null) {
fmb.setPriority(U32.t(Integer.valueOf((String) row.get(Columns.COLUMN_PRIORITY))));
} else if (key.equals(StaticEntryPusher.intructionToColumnName(OFInstructionType.APPLY_ACTIONS)) && fmb != null) {
InstructionUtils.applyActionsFromString(fmb,
(String) row.get(StaticEntryPusher.intructionToColumnName(OFInstructionType.APPLY_ACTIONS)));
} else if (key.equals(StaticEntryPusher.intructionToColumnName(OFInstructionType.CLEAR_ACTIONS)) && fmb != null) {
InstructionUtils.clearActionsFromString(fmb,
(String) row.get(StaticEntryPusher.intructionToColumnName(OFInstructionType.CLEAR_ACTIONS)));
} else if (key.equals(StaticEntryPusher.intructionToColumnName(OFInstructionType.EXPERIMENTER)) && fmb != null) {
InstructionUtils.experimenterFromString(fmb,
(String) row.get(StaticEntryPusher.intructionToColumnName(OFInstructionType.EXPERIMENTER)));
} else if (key.equals(StaticEntryPusher.intructionToColumnName(OFInstructionType.METER)) && fmb != null) {
InstructionUtils.meterFromString(fmb,
(String) row.get(StaticEntryPusher.intructionToColumnName(OFInstructionType.METER)));
} else if (key.equals(StaticEntryPusher.intructionToColumnName(OFInstructionType.GOTO_TABLE)) && fmb != null) {
InstructionUtils.gotoTableFromString(fmb,
(String) row.get(StaticEntryPusher.intructionToColumnName(OFInstructionType.GOTO_TABLE)));
} else if (key.equals(StaticEntryPusher.intructionToColumnName(OFInstructionType.WRITE_ACTIONS)) && fmb != null) {
InstructionUtils.writeActionsFromString(fmb,
(String) row.get(StaticEntryPusher.intructionToColumnName(OFInstructionType.WRITE_ACTIONS)));
} else if (key.equals(StaticEntryPusher.intructionToColumnName(OFInstructionType.WRITE_METADATA)) && fmb != null) {
InstructionUtils.writeMetadataFromString(fmb,
(String) row.get(StaticEntryPusher.intructionToColumnName(OFInstructionType.WRITE_METADATA)));
} else if (key.equals(Columns.COLUMN_GROUP_TYPE) && gmb != null) {
GroupUtils.setGroupTypeFromString(gmb, (String) row.get(Columns.COLUMN_GROUP_TYPE));
} else if (key.equals(Columns.COLUMN_GROUP_BUCKETS) && gmb != null) {
GroupUtils.setGroupBucketsFromJsonArray(gmb, (String) row.get(Columns.COLUMN_GROUP_BUCKETS));
} else if (key.equals(Columns.COLUMN_GROUP_ID) && gmb != null) {
GroupUtils.setGroupIdFromString(gmb, (String) row.get(Columns.COLUMN_GROUP_ID));
} else if (fmb != null) { // the rest of the keys are for Match().fromString()
if (matchString.length() > 0) {
matchString.append(",");
}
matchString.append(key + "=" + row.get(key).toString());
}
}
} catch (Exception e) {
if (entryName != null && switchName != null) {
log.warn("Skipping entry {} on switch {} with bad data : " + e.getMessage(), entryName, switchName);
} else {
log.warn("Skipping entry with bad data: {} :: {} ", e.getMessage(), e.getStackTrace());
}
}
if (fmb != null) {
String match = matchString.toString();
try {
fmb.setMatch(MatchUtils.fromString(match, fmb.getVersion()));
} catch (IllegalArgumentException e) {
log.error(e.toString());
log.error("Ignoring flow entry {} on switch {} with illegal OFMatch() key: " + match, entryName, switchName);
return;
} catch (Exception e) {
log.error("OF version incompatible for the match: " + match);
e.printStackTrace();
return;
}
entries.get(switchName).put(entryName, fmb.build());
} else if (gmb != null) {
entries.get(switchName).put(entryName, gmb.build());
} else {
log.error("Processed neither flow nor group mod. Possible Static Flow Pusher bug");
}
}
@Override
public void switchAdded(DatapathId switchId) {
log.debug("Switch {} connected; processing its static entries",
switchId.toString());
sendEntriesToSwitch(switchId);
}
@Override
public void switchRemoved(DatapathId switchId) {
// do NOT delete from our internal state; we're tracking the rules,
// not the switches
}
@Override
public void switchActivated(DatapathId switchId) {}
@Override
public void switchChanged(DatapathId switchId) {}
@Override
public void switchPortChanged(DatapathId switchId, OFPortDesc port, PortChangeType type) {}
@Override
public void rowsModified(String tableName, Set<Object> rowKeys) {
// This handles both rowInsert() and rowUpdate()
log.debug("Modifying Table {}", tableName);
HashMap<String, Map<String, OFMessage>> entriesToAdd =
new HashMap<String, Map<String, OFMessage>>();
// build up list of what was added
for (Object key: rowKeys) {
IResultSet resultSet = storageSourceService.getRow(tableName, key);
Iterator<IResultSet> it = resultSet.iterator();
while (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, OFMessage>());
List<OFMessage> outQueue = new ArrayList<OFMessage>();
/* For every flow per dpid, decide how to "add" the flow. */
for (String entry : entriesToAdd.get(dpid).keySet()) {
OFFlowMod newFlowMod = null;
OFFlowMod oldFlowMod = null;
OFGroupMod newGroupMod = null;
OFGroupMod oldGroupMod = null;
if (entriesToAdd.get(dpid).get(entry) instanceof OFFlowMod) {
newFlowMod = (OFFlowMod) entriesToAdd.get(dpid).get(entry);
} else if (entriesToAdd.get(dpid).get(entry) instanceof OFGroupMod) {
newGroupMod = (OFGroupMod) entriesToAdd.get(dpid).get(entry);
}
final boolean isFlowMod = newFlowMod == null ? false : true;
String oldDpid = entry2dpid.get(entry);
if (isFlowMod) {
if (oldDpid != null) {
oldFlowMod = (OFFlowMod) entriesFromStorage.get(oldDpid).remove(entry);
}
/* Modify, which can be either a Flow MODIFY_STRICT or a Flow DELETE_STRICT with a side of Flow ADD */
if (oldFlowMod != null && newFlowMod != null) {
/* MODIFY_STRICT b/c the match is still the same */
if (oldFlowMod.getMatch().equals(newFlowMod.getMatch())
&& oldFlowMod.getCookie().equals(newFlowMod.getCookie())
&& oldFlowMod.getPriority() == newFlowMod.getPriority()
&& oldDpid.equalsIgnoreCase(dpid)) {
log.debug("ModifyStrict SFP Flow");
entriesFromStorage.get(dpid).put(entry, newFlowMod);
entry2dpid.put(entry, dpid);
newFlowMod = FlowModUtils.toFlowModifyStrict(newFlowMod);
outQueue.add(newFlowMod);
/* DELETE_STRICT and then ADD b/c the match is now different */
} else {
log.debug("DeleteStrict and Add SFP Flow");
oldFlowMod = FlowModUtils.toFlowDeleteStrict(oldFlowMod);
OFFlowAdd addTmp = FlowModUtils.toFlowAdd(newFlowMod);
/* If the flow's dpid and the current switch we're looking at are the same, add to the queue. */
if (oldDpid.equals(dpid)) {
outQueue.add(oldFlowMod);
outQueue.add(addTmp);
/* Otherwise, go ahead and send the flows now (since queuing them will send to the wrong switch). */
} else {
writeOFMessageToSwitch(DatapathId.of(oldDpid), oldFlowMod);
writeOFMessageToSwitch(DatapathId.of(dpid), FlowModUtils.toFlowAdd(newFlowMod));
}
entriesFromStorage.get(dpid).put(entry, addTmp);
entry2dpid.put(entry, dpid);
}
/* Add a brand-new flow with ADD */
} else if (newFlowMod != null && oldFlowMod == null) {
log.debug("Add SFP Flow");
OFFlowAdd addTmp = FlowModUtils.toFlowAdd(newFlowMod);
entriesFromStorage.get(dpid).put(entry, addTmp);
entry2dpid.put(entry, dpid);
outQueue.add(addTmp);
/* Something strange happened, so remove the flow */
} else if (newFlowMod == null) {
entriesFromStorage.get(dpid).remove(entry);
entry2dpid.remove(entry);
}
} else { /* must be a group mod */
if (oldDpid != null) {
oldGroupMod = (OFGroupMod) entriesFromStorage.get(oldDpid).remove(entry);
}
if (oldGroupMod != null && newGroupMod != null) {
/* Modify */
if (oldGroupMod.getGroup().equals(newGroupMod.getGroup()) &&
oldDpid.equalsIgnoreCase(dpid)) {
log.debug("Modify SFP Group");
entriesFromStorage.get(dpid).put(entry, newGroupMod);
entry2dpid.put(entry, dpid);
newGroupMod = GroupUtils.toGroupModify(newGroupMod);
outQueue.add(newGroupMod);
} else { /* Delete followed by Add */
log.debug("Delete and Add SFP Group");
oldGroupMod = GroupUtils.toGroupDelete(oldGroupMod);
newGroupMod = GroupUtils.toGroupAdd(newGroupMod);
if (oldDpid.equalsIgnoreCase(dpid)) {
outQueue.add(oldGroupMod);
outQueue.add(newGroupMod);
} else {
writeOFMessageToSwitch(DatapathId.of(oldDpid), oldGroupMod);
writeOFMessageToSwitch(DatapathId.of(dpid), newGroupMod);
}
entriesFromStorage.get(dpid).put(entry, newGroupMod);
entry2dpid.put(entry, dpid);
}
/* Add */
} else if (oldGroupMod == null && newGroupMod != null) {
log.debug("Add SFP Group");
newGroupMod = GroupUtils.toGroupAdd(newGroupMod);
entriesFromStorage.get(dpid).put(entry, newGroupMod);
entry2dpid.put(entry, dpid);
outQueue.add(newGroupMod);
} else {
/* Something strange happened; remove group */
entriesFromStorage.get(dpid).remove(entry);
entry2dpid.remove(entry);
}
}
}
/* Batch-write all queued messages to the switch */
writeOFMessagesToSwitch(DatapathId.of(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);
}
}
private void deleteStaticFlowEntry(String entryName) {
String dpid = entry2dpid.remove(entryName);
if (dpid == null) {
// assume state has been cleared by deleteFlowsForSwitch() or
// deleteAllFlows()
return;
}
if (log.isDebugEnabled()) {
log.debug("Sending delete flow mod for flow {} for switch {}", entryName, dpid);
}
// send flow_mod delete
if (switchService.getSwitch(DatapathId.of(dpid)) != null) {
OFMessage message = entriesFromStorage.get(dpid).get(entryName);
if (message instanceof OFFlowMod) {
message = FlowModUtils.toFlowDeleteStrict((OFFlowMod) message);
} else if (message instanceof OFGroupMod) {
message = GroupUtils.toGroupDelete((OFGroupMod) message);
}
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;
}
writeOFMessageToSwitch(DatapathId.of(dpid), message);
} else {
log.debug("Not sending flow delete for disconnected switch.");
}
return;
}
/**
* 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.
*/
private void writeOFMessagesToSwitch(DatapathId dpid, List<OFMessage> messages) {
IOFSwitch ofswitch = switchService.getSwitch(dpid);
if (ofswitch != null) { // is the switch connected
if (log.isDebugEnabled()) {
log.debug("Sending {} new entries to {}", messages.size(), dpid);
}
ofswitch.write(messages);
}
}
/**
* Writes a single OFMessage to a switch
* @param dpid The datapath ID of the switch to write to
* @param message The OFMessage to write.
*/
private void writeOFMessageToSwitch(DatapathId dpid, OFMessage message) {
IOFSwitch ofswitch = switchService.getSwitch(dpid);
if (ofswitch != null) { // is the switch connected
if (log.isDebugEnabled()) {
log.debug("Sending 1 new entries to {}", dpid.toString());
}
ofswitch.write(message);
}
}
@Override
public String getName() {
return MODULE_NAME;
}
/**
* Handles a flow removed message from a switch. If the flow was removed
* and we did not explicitly delete it we re-install it. If we explicitly
* removed the flow we stop the processing of the flow removed message.
* @param sw The switch that sent the flow removed message.
* @param msg The flow removed message.
* @param cntx The associated context.
* @return Whether to continue processing this message.
*/
public Command handleFlowRemoved(IOFSwitch sw, OFFlowRemoved msg, FloodlightContext cntx) {
U64 cookie = msg.getCookie();
if (AppCookie.extractApp(cookie) == STATIC_ENTRY_APP_ID) {
OFFlowRemovedReason reason = null;
reason = msg.getReason();
if (reason != null) {
if (OFFlowRemovedReason.DELETE == reason) {
log.debug("Received flow_removed message for a infinite " +
"timeout flow from switch {}. Removing it from the SFP DB", msg, sw);
} else if (OFFlowRemovedReason.HARD_TIMEOUT == reason || OFFlowRemovedReason.IDLE_TIMEOUT == reason) {
/* Remove the Flow from the DB since it timed out */
log.debug("Received an IDLE or HARD timeout for an SFP flow. Removing it from the SFP DB");
} else {
log.debug("Received flow_removed message for reason {}. Removing it from the SFP DB", reason);
}
/*
* Lookup the flow based on the flow contents. We do not know/care about the name of the
* flow based on this message, but we can get the table values for this switch and search.
*/
String flowToRemove = null;
Map<String, OFMessage> flowsByName = getEntries(sw.getId())
.entrySet()
.stream()
.filter(e -> e.getValue() instanceof OFFlowMod)
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
for (Map.Entry<String, OFMessage> e : flowsByName.entrySet()) {
OFFlowMod f = (OFFlowMod) e.getValue();
if (msg.getCookie().equals(f.getCookie()) &&
(msg.getVersion().compareTo(OFVersion.OF_12) < 0 ? true : msg.getHardTimeout() == f.getHardTimeout()) &&
msg.getIdleTimeout() == f.getIdleTimeout() &&
msg.getMatch().equals(f.getMatch()) &&
msg.getPriority() == f.getPriority() &&
(msg.getVersion().compareTo(OFVersion.OF_10) == 0 ? true : msg.getTableId().equals(f.getTableId()))
) {
flowToRemove = e.getKey();
break;
}
}
/*
* Remove the flow. This will send the delete message to the switch,
* since we cannot tell the storage listener rowsdeleted() that we
* are only removing our local DB copy of the flow and that it actually
* timed out on the switch and is already gone. The switch will silently
* discard the delete message in this case.
*
* TODO: We should come up with a way to convey to the storage listener
* the reason for the flow being removed.
*/
if (flowToRemove != null) {
log.warn("Removing flow {} for reason {}", flowToRemove, reason);
deleteEntry(flowToRemove);
}
/* Stop the processing chain since we sent or asked for the delete message. */
return Command.STOP;
}
}
/* Continue the processing chain, since we did not send the delete. */
return Command.CONTINUE;
}
@Override
public Command receive(IOFSwitch sw, OFMessage msg, FloodlightContext cntx) {
switch (msg.getType()) {
case FLOW_REMOVED:
return handleFlowRemoved(sw, (OFFlowRemoved) msg, cntx);
default:
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(IStaticEntryPusherService.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(IStaticEntryPusherService.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(IOFSwitchService.class);
l.add(IStorageSourceService.class);
l.add(IRestApiService.class);
return l;
}
private void populateColumns() {
Set<String> tmp = new HashSet<String>();
tmp.add(Columns.COLUMN_NAME);
tmp.add(Columns.COLUMN_ENTRY_TYPE);
tmp.add(Columns.COLUMN_SWITCH);
tmp.add(Columns.COLUMN_TABLE_ID);
tmp.add(Columns.COLUMN_ACTIVE);
tmp.add(Columns.COLUMN_IDLE_TIMEOUT);
tmp.add(Columns.COLUMN_HARD_TIMEOUT);
tmp.add(Columns.COLUMN_PRIORITY);
tmp.add(Columns.COLUMN_COOKIE);
tmp.add(Columns.COLUMN_TP_SRC);
tmp.add(Columns.COLUMN_TP_DST);
tmp.add(Columns.COLUMN_ACTIONS);
tmp.add(Columns.COLUMN_GROUP_TYPE);
tmp.add(Columns.COLUMN_GROUP_BUCKETS);
tmp.add(Columns.COLUMN_GROUP_ID);
for (MatchFields m : MatchFields.values()) {
/* skip all BSN_* matches */
if (!m.name().toLowerCase().startsWith("bsn")) {
tmp.add(StaticEntryPusher.matchFieldToColumnName(m));
}
}
for (OFInstructionType t : OFInstructionType.values()) {
tmp.add(StaticEntryPusher.intructionToColumnName(t));
}
Columns.ALL_COLUMNS = ImmutableSet.copyOf(tmp);
}
@Override
public void init(FloodlightModuleContext context) throws FloodlightModuleException {
populateColumns();
floodlightProviderService = context.getServiceImpl(IFloodlightProviderService.class);
switchService = context.getServiceImpl(IOFSwitchService.class);
storageSourceService = context.getServiceImpl(IStorageSourceService.class);
restApiService = context.getServiceImpl(IRestApiService.class);
haListener = new HAListenerDelegate();
}
@Override
public void startUp(FloodlightModuleContext context) {
floodlightProviderService.addOFMessageListener(OFType.FLOW_REMOVED, this);
switchService.addOFSwitchListener(this);
floodlightProviderService.addHAListener(this.haListener);
// assumes no switches connected at startup()
storageSourceService.createTable(TABLE_NAME, null);
storageSourceService.setTablePrimaryKeyName(TABLE_NAME, Columns.COLUMN_NAME);
storageSourceService.addListener(TABLE_NAME, this);
entriesFromStorage = readEntriesFromStorage();
entry2dpid = computeEntry2DpidMap(entriesFromStorage);
restApiService.addRestletRoutable(new StaticEntryWebRoutable()); /* current */
restApiService.addRestletRoutable(new StaticFlowWebRoutable()); /* v1.0 - v1.2 (v1.3?) */
restApiService.addRestletRoutable(new StaticFlowEntryWebRoutable()); /* v0.91, v0.90, and before */
}
// IStaticFlowEntryPusherService methods
@Override
public void addFlow(String name, OFFlowMod fm, DatapathId swDpid) {
try {
Map<String, Object> fmMap = StaticEntries.flowModToStorageEntry(fm, swDpid.toString(), name);
storageSourceService.insertRowAsync(TABLE_NAME, fmMap);
} catch (Exception e) {
log.error("Did not add flow with bad match/action combination. {}", fm);
}
}
@Override
public void addGroup(String name, OFGroupMod gm, DatapathId swDpid) {
try {
Map<String, Object> gmMap = StaticEntries.groupModToStorageEntry(gm, swDpid.toString(), name);
storageSourceService.insertRowAsync(TABLE_NAME, gmMap);
} catch (Exception e) {
log.error("Did not add group with bad match/action combination. {}", gm);
}
}
@Override
public void deleteEntry(String name) {
storageSourceService.deleteRowAsync(TABLE_NAME, name);
}
@Override
public void deleteAllEntries() {
for (String entry : entry2dpid.keySet()) {
deleteEntry(entry);
}
/*
FIXME: Since the OF spec 1.0 is not clear on how
to match on cookies. Once all switches come to a
common implementation we can possibly re-enable this
fix.
// Send a delete for each switch
Set<String> swSet = new HashSet<String>();
for (String dpid : entry2dpid.values()) {
// Avoid sending duplicate deletes
if (!swSet.contains(dpid)) {
swSet.add(dpid);
sendDeleteByCookie(HexString.toLong(dpid));
}
}
// Clear our map
entry2dpid.clear();
// Clear our book keeping map
for (Map<String, OFFlowMod> eMap : entriesFromStorage.values()) {
eMap.clear();
}
// Reset our DB
storageSource.deleteMatchingRowsAsync(TABLE_NAME, null);
*/
}
@Override
public void deleteEntriesForSwitch(DatapathId dpid) {
String sDpid = dpid.toString();
for (Entry<String, String> e : entry2dpid.entrySet()) {
if (e.getValue().equals(sDpid))
deleteEntry(e.getKey());
}
/*
FIXME: Since the OF spec 1.0 is not clear on how
to match on cookies. Once all switches come to a
common implementation we can possibly re-enable this
fix.
//sendDeleteByCookie(dpid);
String sDpid = HexString.toHexString(dpid);
// Clear all internal flows for this switch
Map<String, OFFlowMod> sMap = entriesFromStorage.get(sDpid);
if (sMap != null) {
for (String entryName : sMap.keySet()) {
entry2dpid.remove(entryName);
// Delete from DB
deleteFlow(entryName);
}
sMap.clear();
} else {
log.warn("Map of storage entries for switch {} was null", sDpid);
}
*/
}
/**
* Deletes all flows installed by static flow pusher on a given switch.
* We send a delete flow mod with the static flow pusher app ID in the cookie.
* Since OF1.0 doesn't support masking based on the cookie we have to
* disable having flow specific cookies.
* @param dpid The DPID of the switch to clear all it's flows.
*/
/*
FIXME: Since the OF spec 1.0 is not clear on how
to match on cookies. Once all switches come to a
common implementation we can possibly re-enable this
fix.
private void sendDeleteByCookie(long dpid) {
if (log.isDebugEnabled())
log.debug("Deleting all static flows on switch {}", HexString.toHexString(dpid));
IOFSwitch sw = floodlightProvider.getSwitch(dpid);
if (sw == null) {
log.warn("Tried to delete static flows for non-existant switch {}",
HexString.toHexString(dpid));
return;
}
OFFlowMod fm = (OFFlowMod) floodlightProvider.getOFMessageFactory().
getMessage(OFType.FLOW_MOD);
OFMatch ofm = new OFMatch();
fm.setMatch(ofm);
fm.setCookie(AppCookie.makeCookie(StaticFlowEntryPusher.STATIC_FLOW_APP_ID, 0));
fm.setCommand(OFFlowMod.OFPFC_DELETE);
fm.setOutPort(OFPort.OFPP_NONE);
try {
sw.write(fm, null);
sw.flush();
} catch (IOException e1) {
log.error("Error deleting all flows for switch {}:\n {}",
HexString.toHexString(dpid), e1.getMessage());
return;
}
}
*/
@Override
public Map<String, Map<String, OFMessage>> getEntries() {
return entriesFromStorage;
}
@Override
public Map<String, OFMessage> getEntries(DatapathId dpid) {
Map<String, OFMessage> m = entriesFromStorage.get(dpid.toString());
return m == null ? Collections.emptyMap() : m;
}
// IHAListener
private class HAListenerDelegate implements IHAListener {
@Override
public void transitionToActive() {
log.debug("Re-reading static flows from storage due " +
"to HA change from STANDBY->ACTIVE");
entriesFromStorage = readEntriesFromStorage();
entry2dpid = computeEntry2DpidMap(entriesFromStorage);
}
@Override
public void controllerNodeIPsChanged(
Map<String, String> curControllerNodeIPs,
Map<String, String> addedControllerNodeIPs,
Map<String, String> removedControllerNodeIPs) {
// ignore
}
@Override
public String getName() {
return StaticEntryPusher.this.getName();
}
@Override
public boolean isCallbackOrderingPrereq(HAListenerTypeMarker type,
String name) {
return false;
}
@Override
public boolean isCallbackOrderingPostreq(HAListenerTypeMarker type,
String name) {
return false;
}
@Override
public void transitionToStandby() {
log.debug("Controller is now in STANDBY role. Clearing static flow entries from store.");
deleteAllEntries();
}
}
@Override
public void switchDeactivated(DatapathId switchId) {
}
}