Package net.floodlightcontroller.devicemanager.internal

Source Code of net.floodlightcontroller.devicemanager.internal.DeviceManagerImpl$HAListenerDelegate

/**
*    Copyright 2011,2012 Big Switch Networks, Inc.
*    Originally created by David Erickson, Stanford University
*
*    Licensed under the Apache License, Version 2.0 (the "License"); you may
*    not use this file except in compliance with the License. You may obtain
*    a copy of the License at
*
*         http://www.apache.org/licenses/LICENSE-2.0
*
*    Unless required by applicable law or agreed to in writing, software
*    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
*    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
*    License for the specific language governing permissions and limitations
*    under the License.
**/

package net.floodlightcontroller.devicemanager.internal;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import net.floodlightcontroller.core.FloodlightContext;
import net.floodlightcontroller.core.HAListenerTypeMarker;
import net.floodlightcontroller.core.IFloodlightProviderService;
import net.floodlightcontroller.core.IHAListener;
import net.floodlightcontroller.core.IInfoProvider;
import net.floodlightcontroller.core.IOFMessageListener;
import net.floodlightcontroller.core.IOFSwitch;
import net.floodlightcontroller.core.IFloodlightProviderService.Role;
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.ListenerDispatcher;
import net.floodlightcontroller.core.util.SingletonTask;
import net.floodlightcontroller.debugcounter.IDebugCounter;
import net.floodlightcontroller.debugcounter.IDebugCounterService;
import net.floodlightcontroller.debugcounter.IDebugCounterService.CounterException;
import net.floodlightcontroller.debugcounter.NullDebugCounter;
import net.floodlightcontroller.debugcounter.IDebugCounterService.CounterType;
import net.floodlightcontroller.debugevent.IDebugEventService;
import net.floodlightcontroller.debugevent.IDebugEventService.EventColumn;
import net.floodlightcontroller.debugevent.IDebugEventService.EventFieldType;
import net.floodlightcontroller.debugevent.IDebugEventService.EventType;
import net.floodlightcontroller.debugevent.IDebugEventService.MaxEventsRegistered;
import net.floodlightcontroller.debugevent.IEventUpdater;
import net.floodlightcontroller.debugevent.NullDebugEvent;
import net.floodlightcontroller.devicemanager.IDevice;
import net.floodlightcontroller.devicemanager.IDeviceService;
import net.floodlightcontroller.devicemanager.IEntityClass;
import net.floodlightcontroller.devicemanager.IEntityClassListener;
import net.floodlightcontroller.devicemanager.IEntityClassifierService;
import net.floodlightcontroller.devicemanager.IDeviceListener;
import net.floodlightcontroller.devicemanager.SwitchPort;
import net.floodlightcontroller.devicemanager.internal.DeviceSyncRepresentation.SyncEntity;
import net.floodlightcontroller.devicemanager.web.DeviceRoutable;
import net.floodlightcontroller.flowcache.IFlowReconcileEngineService;
import net.floodlightcontroller.flowcache.IFlowReconcileListener;
import net.floodlightcontroller.flowcache.IFlowReconcileService;
import net.floodlightcontroller.flowcache.OFMatchReconcile;
import net.floodlightcontroller.linkdiscovery.ILinkDiscovery.LDUpdate;
import net.floodlightcontroller.packet.ARP;
import net.floodlightcontroller.packet.DHCP;
import net.floodlightcontroller.packet.DHCPOption;
import net.floodlightcontroller.packet.Ethernet;
import net.floodlightcontroller.packet.IPv4;
import net.floodlightcontroller.packet.UDP;
import net.floodlightcontroller.packet.DHCP.DHCPOptionCode;
import net.floodlightcontroller.restserver.IRestApiService;
import net.floodlightcontroller.storage.IStorageSourceService;
import net.floodlightcontroller.threadpool.IThreadPoolService;
import net.floodlightcontroller.topology.ITopologyListener;
import net.floodlightcontroller.topology.ITopologyService;
import net.floodlightcontroller.util.MultiIterator;
import static net.floodlightcontroller.devicemanager.internal.
DeviceManagerImpl.DeviceUpdate.Change.*;

import org.openflow.protocol.OFMatchWithSwDpid;
import org.openflow.protocol.OFMessage;
import org.openflow.protocol.OFPacketIn;
import org.openflow.protocol.OFPort;
import org.openflow.protocol.OFType;
import org.sdnplatform.sync.IClosableIterator;
import org.sdnplatform.sync.IStoreClient;
import org.sdnplatform.sync.ISyncService;
import org.sdnplatform.sync.ISyncService.Scope;
import org.sdnplatform.sync.Versioned;
import org.sdnplatform.sync.error.ObsoleteVersionException;
import org.sdnplatform.sync.error.SyncException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* DeviceManager creates Devices based upon MAC addresses seen in the network.
* It tracks any network addresses mapped to the Device, and its location
* within the network.
* @author readams
*/
public class DeviceManagerImpl implements
IDeviceService, IOFMessageListener, ITopologyListener,
IFloodlightModule, IEntityClassListener,
IFlowReconcileListener, IInfoProvider {
    protected static Logger logger =
            LoggerFactory.getLogger(DeviceManagerImpl.class);


    protected IFloodlightProviderService floodlightProvider;
    protected ITopologyService topology;
    protected IStorageSourceService storageSource;
    protected IRestApiService restApi;
    protected IThreadPoolService threadPool;
    protected IFlowReconcileService flowReconcileMgr;
    protected IFlowReconcileEngineService flowReconcileEngine;
    protected IDebugCounterService debugCounters;
    private ISyncService syncService;
    private IStoreClient<String,DeviceSyncRepresentation> storeClient;
    private DeviceSyncManager deviceSyncManager;

    /**
     * Debug Counters
     */
    public static final String MODULE_NAME = "devicemanager";
    public static final String PACKAGE = DeviceManagerImpl.class.getPackage().getName();
    public IDebugCounter cntIncoming;
    public IDebugCounter cntReconcileRequest;
    public IDebugCounter cntReconcileNoSource;
    public IDebugCounter cntReconcileNoDest;
    public IDebugCounter cntInvalidSource;
    public IDebugCounter cntInvalidDest;
    public IDebugCounter cntNoSource;
    public IDebugCounter cntNoDest;
    public IDebugCounter cntDhcpClientNameSnooped;
    public IDebugCounter cntDeviceOnInternalPortNotLearned;
    public IDebugCounter cntPacketNotAllowed;
    public IDebugCounter cntNewDevice;
    public IDebugCounter cntPacketOnInternalPortForKnownDevice;
    public IDebugCounter cntNewEntity;
    public IDebugCounter cntDeviceChanged;
    public IDebugCounter cntDeviceMoved;
    public IDebugCounter cntCleanupEntitiesRuns;
    public IDebugCounter cntEntityRemovedTimeout;
    public IDebugCounter cntDeviceDeleted;
    public IDebugCounter cntDeviceReclassifyDelete;
    public IDebugCounter cntDeviceStrored;
    public IDebugCounter cntDeviceStoreThrottled;
    public IDebugCounter cntDeviceRemovedFromStore;
    public IDebugCounter cntSyncException;
    public IDebugCounter cntDevicesFromStore;
    public IDebugCounter cntConsolidateStoreRuns;
    public IDebugCounter cntConsolidateStoreDevicesRemoved;
    public IDebugCounter cntTransitionToMaster;

    /**
     * Debug Events
     */
    private IDebugEventService debugEvents;
    private IEventUpdater<DeviceEvent> evDevice;

    private boolean isMaster = false;

    static final String DEVICE_SYNC_STORE_NAME =
            DeviceManagerImpl.class.getCanonicalName() + ".stateStore";

    /**
     * Time interval between writes of entries for the same device to
     * the sync store.
     */
    static final int DEFAULT_SYNC_STORE_WRITE_INTERVAL_MS =
            5*60*1000; // 5 min
    private int syncStoreWriteIntervalMs = DEFAULT_SYNC_STORE_WRITE_INTERVAL_MS;

    /**
     * Time after SLAVE->MASTER until we run the consolidate store
     * code.
     */
    static final int DEFAULT_INITIAL_SYNC_STORE_CONSOLIDATE_MS =
            15*1000; // 15 sec
    private int initialSyncStoreConsolidateMs =
            DEFAULT_INITIAL_SYNC_STORE_CONSOLIDATE_MS;

    /**
     * Time interval between consolidate store runs.
     */
    static final int DEFAULT_SYNC_STORE_CONSOLIDATE_INTERVAL_MS =
            75*60*1000; // 75 min
    private final int syncStoreConsolidateIntervalMs =
            DEFAULT_SYNC_STORE_CONSOLIDATE_INTERVAL_MS;

    /**
     * Time in milliseconds before entities will expire
     */
    protected static final int ENTITY_TIMEOUT = 60*60*1000;

    /**
     * Time in seconds between cleaning up old entities/devices
     */
    protected static final int ENTITY_CLEANUP_INTERVAL = 60*60;

    /**
     * This is the master device map that maps device IDs to {@link Device}
     * objects.
     */
    protected ConcurrentHashMap<Long, Device> deviceMap;

    /**
     * Counter used to generate device keys
     */
    protected AtomicLong deviceKeyCounter = new AtomicLong(0);

    /**
     * This is the primary entity index that contains all entities
     */
    protected DeviceUniqueIndex primaryIndex;

    /**
     * This stores secondary indices over the fields in the devices
     */
    protected Map<EnumSet<DeviceField>, DeviceIndex> secondaryIndexMap;

    /**
     * This map contains state for each of the {@ref IEntityClass}
     * that exist
     */
    protected ConcurrentHashMap<String, ClassState> classStateMap;

    /**
     * This is the list of indices we want on a per-class basis
     */
    protected Set<EnumSet<DeviceField>> perClassIndices;

    /**
     * The entity classifier currently in use
     */
    protected IEntityClassifierService entityClassifier;

    /**
     * Used to cache state about specific entity classes
     */
    protected class ClassState {

        /**
         * The class index
         */
        protected DeviceUniqueIndex classIndex;

        /**
         * This stores secondary indices over the fields in the device for the
         * class
         */
        protected Map<EnumSet<DeviceField>, DeviceIndex> secondaryIndexMap;

        /**
         * Allocate a new {@link ClassState} object for the class
         * @param clazz the class to use for the state
         */
        public ClassState(IEntityClass clazz) {
            EnumSet<DeviceField> keyFields = clazz.getKeyFields();
            EnumSet<DeviceField> primaryKeyFields =
                    entityClassifier.getKeyFields();
            boolean keyFieldsMatchPrimary =
                    primaryKeyFields.equals(keyFields);

            if (!keyFieldsMatchPrimary)
                classIndex = new DeviceUniqueIndex(keyFields);

            secondaryIndexMap =
                    new HashMap<EnumSet<DeviceField>, DeviceIndex>();
            for (EnumSet<DeviceField> fields : perClassIndices) {
                secondaryIndexMap.put(fields,
                                      new DeviceMultiIndex(fields));
            }
        }
    }

    /**
     * Device manager event listeners
     * reclassifyDeviceListeners are notified first before reconcileDeviceListeners.
     * This is to make sure devices are correctly reclassified before reconciliation.
     */
    protected ListenerDispatcher<String,IDeviceListener> deviceListeners;

    /**
     * A device update event to be dispatched
     */
    protected static class DeviceUpdate {
        public enum Change {
            ADD, DELETE, CHANGE;
        }

        /**
         * The affected device
         */
        protected Device device;

        /**
         * The change that was made
         */
        protected Change change;

        /**
         * If not added, then this is the list of fields changed
         */
        protected EnumSet<DeviceField> fieldsChanged;

        public DeviceUpdate(Device device, Change change,
                            EnumSet<DeviceField> fieldsChanged) {
            super();
            this.device = device;
            this.change = change;
            this.fieldsChanged = fieldsChanged;
        }

        @Override
        public String toString() {
            String devIdStr = device.getEntityClass().getName() + "::" +
                    device.getMACAddressString();
            return "DeviceUpdate [device=" + devIdStr + ", change=" + change
                   + ", fieldsChanged=" + fieldsChanged + "]";
        }

    }

    /**
     * AttachmentPointComparator
     *
     * Compares two attachment points and returns the latest one.
     * It is assumed that the two attachment points are in the same
     * L2 domain.
     *
     * @author srini
     */
    protected class AttachmentPointComparator
    implements Comparator<AttachmentPoint> {
        public AttachmentPointComparator() {
            super();
        }

        @Override
        public int compare(AttachmentPoint oldAP, AttachmentPoint newAP) {
            //First compare based on L2 domain ID;

            long oldSw = oldAP.getSw();
            short oldPort = oldAP.getPort();
            long oldDomain = topology.getL2DomainId(oldSw);
            boolean oldBD = topology.isBroadcastDomainPort(oldSw, oldPort);

            long newSw = newAP.getSw();
            short newPort = newAP.getPort();
            long newDomain = topology.getL2DomainId(newSw);
            boolean newBD = topology.isBroadcastDomainPort(newSw, newPort);

            if (oldDomain < newDomain) return -1;
            else if (oldDomain > newDomain) return 1;


            // Give preference to OFPP_LOCAL always
            if (oldPort != OFPort.OFPP_LOCAL.getValue() &&
                    newPort == OFPort.OFPP_LOCAL.getValue()) {
                return -1;
            } else if (oldPort == OFPort.OFPP_LOCAL.getValue() &&
                    newPort != OFPort.OFPP_LOCAL.getValue()) {
                return 1;
            }

            // We expect that the last seen of the new AP is higher than
            // old AP, if it is not, just reverse and send the negative
            // of the result.
            if (oldAP.getActiveSince() > newAP.getActiveSince())
                return -compare(newAP, oldAP);

            long activeOffset = 0;
            if (!topology.isConsistent(oldSw, oldPort, newSw, newPort)) {
                if (!newBD && oldBD) {
                    return -1;
                }
                if (newBD && oldBD) {
                    activeOffset = AttachmentPoint.EXTERNAL_TO_EXTERNAL_TIMEOUT;
                }
                else if (newBD && !oldBD){
                    activeOffset = AttachmentPoint.OPENFLOW_TO_EXTERNAL_TIMEOUT;
                }

            } else {
                // The attachment point is consistent.
                activeOffset = AttachmentPoint.CONSISTENT_TIMEOUT;
            }


            if ((newAP.getActiveSince() > oldAP.getLastSeen() + activeOffset) ||
                    (newAP.getLastSeen() > oldAP.getLastSeen() +
                            AttachmentPoint.INACTIVITY_INTERVAL)) {
                return -1;
            }
            return 1;
        }
    }
    /**
     * Comparator for sorting by cluster ID
     */
    public AttachmentPointComparator apComparator;

    /**
     * Switch ports where attachment points shouldn't be learned
     */
    private Set<SwitchPort> suppressAPs;

    /**
     * Periodic task to clean up expired entities
     */
    public SingletonTask entityCleanupTask;


    /**
     * Periodic task to consolidate entries in the store. I.e., delete
     * entries in the store that are not known to DeviceManager
     */
    private SingletonTask storeConsolidateTask;

    /**
     * Listens for HA notifications
     */
    protected HAListenerDelegate haListenerDelegate;


    // *********************
    // IDeviceManagerService
    // *********************

    @Override
    public IDevice getDevice(Long deviceKey) {
        return deviceMap.get(deviceKey);
    }

    @Override
    public IDevice findDevice(long macAddress, Short vlan,
                              Integer ipv4Address, Long switchDPID,
                              Integer switchPort)
                              throws IllegalArgumentException {
        if (vlan != null && vlan.shortValue() <= 0)
            vlan = null;
        if (ipv4Address != null && ipv4Address == 0)
            ipv4Address = null;
        Entity e = new Entity(macAddress, vlan, ipv4Address, switchDPID,
                              switchPort, null);
        if (!allKeyFieldsPresent(e, entityClassifier.getKeyFields())) {
            throw new IllegalArgumentException("Not all key fields specified."
                      + " Required fields: " + entityClassifier.getKeyFields());
        }
        return findDeviceByEntity(e);
    }

    @Override
    public IDevice findClassDevice(IEntityClass entityClass, long macAddress,
                                  Short vlan, Integer ipv4Address)
                                  throws IllegalArgumentException {
        if (vlan != null && vlan.shortValue() <= 0)
            vlan = null;
        if (ipv4Address != null && ipv4Address == 0)
            ipv4Address = null;
        Entity e = new Entity(macAddress, vlan, ipv4Address,
                              null, null, null);
        if (entityClass == null ||
                !allKeyFieldsPresent(e, entityClass.getKeyFields())) {
            throw new IllegalArgumentException("Not all key fields and/or "
                    + " no source device specified. Required fields: " +
                    entityClassifier.getKeyFields());
        }
        return findDestByEntity(entityClass, e);
    }

    @Override
    public Collection<? extends IDevice> getAllDevices() {
        return Collections.unmodifiableCollection(deviceMap.values());
    }

    @Override
    public void addIndex(boolean perClass,
                         EnumSet<DeviceField> keyFields) {
        if (perClass) {
            perClassIndices.add(keyFields);
        } else {
            secondaryIndexMap.put(keyFields,
                                  new DeviceMultiIndex(keyFields));
        }
    }

    @Override
    public Iterator<? extends IDevice> queryDevices(Long macAddress,
                                                    Short vlan,
                                                    Integer ipv4Address,
                                                    Long switchDPID,
                                                    Integer switchPort) {
        DeviceIndex index = null;
        if (secondaryIndexMap.size() > 0) {
            EnumSet<DeviceField> keys =
                    getEntityKeys(macAddress, vlan, ipv4Address,
                                  switchDPID, switchPort);
            index = secondaryIndexMap.get(keys);
        }

        Iterator<Device> deviceIterator = null;
        if (index == null) {
            // Do a full table scan
            deviceIterator = deviceMap.values().iterator();
        } else {
            // index lookup
            Entity entity = new Entity((macAddress == null ? 0 : macAddress),
                                       vlan,
                                       ipv4Address,
                                       switchDPID,
                                       switchPort,
                                       null);
            deviceIterator =
                    new DeviceIndexInterator(this, index.queryByEntity(entity));
        }

        DeviceIterator di =
                new DeviceIterator(deviceIterator,
                                   null,
                                   macAddress,
                                   vlan,
                                   ipv4Address,
                                   switchDPID,
                                   switchPort);
        return di;
    }

    @Override
    public Iterator<? extends IDevice> queryClassDevices(IEntityClass entityClass,
                                                         Long macAddress,
                                                         Short vlan,
                                                         Integer ipv4Address,
                                                         Long switchDPID,
                                                         Integer switchPort) {
        ArrayList<Iterator<Device>> iterators =
                new ArrayList<Iterator<Device>>();
        ClassState classState = getClassState(entityClass);

        DeviceIndex index = null;
        if (classState.secondaryIndexMap.size() > 0) {
            EnumSet<DeviceField> keys =
                    getEntityKeys(macAddress, vlan, ipv4Address,
                                  switchDPID, switchPort);
            index = classState.secondaryIndexMap.get(keys);
        }

        Iterator<Device> iter;
        if (index == null) {
            index = classState.classIndex;
            if (index == null) {
                // scan all devices
                return new DeviceIterator(deviceMap.values().iterator(),
                                          new IEntityClass[] { entityClass },
                                          macAddress, vlan, ipv4Address,
                                          switchDPID, switchPort);
            } else {
                // scan the entire class
                iter = new DeviceIndexInterator(this, index.getAll());
            }
        } else {
            // index lookup
            Entity entity =
                    new Entity((macAddress == null ? 0 : macAddress),
                               vlan,
                               ipv4Address,
                               switchDPID,
                               switchPort,
                               null);
            iter = new DeviceIndexInterator(this,
                                            index.queryByEntity(entity));
        }
        iterators.add(iter);

        return new MultiIterator<Device>(iterators.iterator());
    }

    protected Iterator<Device> getDeviceIteratorForQuery(Long macAddress,
                                                        Short vlan,
                                                        Integer ipv4Address,
                                                        Long switchDPID,
                                                        Integer switchPort) {
        DeviceIndex index = null;
        if (secondaryIndexMap.size() > 0) {
            EnumSet<DeviceField> keys =
                getEntityKeys(macAddress, vlan, ipv4Address,
                            switchDPID, switchPort);
            index = secondaryIndexMap.get(keys);
        }

        Iterator<Device> deviceIterator = null;
        if (index == null) {
            // Do a full table scan
            deviceIterator = deviceMap.values().iterator();
        } else {
            // index lookup
            Entity entity = new Entity((macAddress == null ? 0 : macAddress),
                                vlan,
                                ipv4Address,
                                switchDPID,
                                switchPort,
                                null);
            deviceIterator =
                new DeviceIndexInterator(this, index.queryByEntity(entity));
        }

        DeviceIterator di =
            new DeviceIterator(deviceIterator,
                                null,
                                macAddress,
                                vlan,
                                ipv4Address,
                                switchDPID,
                                switchPort);
        return di;
    }

    @Override
    public void addListener(IDeviceListener listener) {
         deviceListeners.addListener("device", listener);
         logListeners();
    }

    @Override
    public void addSuppressAPs(long swId, short port) {
        suppressAPs.add(new SwitchPort(swId, port));
    }

    @Override
    public void removeSuppressAPs(long swId, short port) {
        suppressAPs.remove(new SwitchPort(swId, port));
    }

    @Override
    public Set<SwitchPort> getSuppressAPs() {
        return Collections.unmodifiableSet(suppressAPs);
    }

    private void logListeners() {
        List<IDeviceListener> listeners = deviceListeners.getOrderedListeners();
        if (listeners != null) {
            StringBuffer sb = new StringBuffer();
            sb.append("DeviceListeners: ");
            for (IDeviceListener l : listeners) {
                sb.append(l.getName());
                sb.append(",");
            }
            logger.debug(sb.toString());
        }
    }

    // ***************
    // IDeviceListener
    // ***************
    private class DeviceDebugEventLogger implements IDeviceListener {
        @Override
        public String getName() {
            return "deviceDebugEventLogger";
        }

        @Override
        public boolean isCallbackOrderingPrereq(String type, String name) {
            return false;
        }

        @Override
        public boolean isCallbackOrderingPostreq(String type, String name) {
            return false;
        }

        @Override
        public void deviceAdded(IDevice device) {
            generateDeviceEvent(device, "host-added");
        }

        @Override
        public void deviceRemoved(IDevice device) {
            generateDeviceEvent(device, "host-removed");
        }

        @Override
        public void deviceMoved(IDevice device) {
            generateDeviceEvent(device, "host-moved");
        }

        @Override
        public void deviceIPV4AddrChanged(IDevice device) {
            generateDeviceEvent(device, "host-ipv4-addr-changed");
        }

        @Override
        public void deviceVlanChanged(IDevice device) {
            generateDeviceEvent(device, "host-vlan-changed");
        }

        private void generateDeviceEvent(IDevice device, String reason) {
            List<Integer> ipv4Addresses =
                new ArrayList<Integer>(Arrays.asList(device.getIPv4Addresses()));
            List<SwitchPort> oldAps =
                new ArrayList<SwitchPort>(Arrays.asList(device.getOldAP()));
            List<SwitchPort> currentAps =
                    new ArrayList<SwitchPort>(Arrays.asList(device.getAttachmentPoints()));
            List<Short> vlanIds =
                    new ArrayList<Short>(Arrays.asList(device.getVlanId()));

            evDevice.updateEventNoFlush(
                    new DeviceEvent(device.getMACAddress(),
                                    ipv4Addresses,
                                    oldAps,
                                    currentAps,
                                    vlanIds, reason));
        }
    }

    // *************
    // IInfoProvider
    // *************

    @Override
    public Map<String, Object> getInfo(String type) {
        if (!"summary".equals(type))
            return null;

        Map<String, Object> info = new HashMap<String, Object>();
        info.put("# hosts", deviceMap.size());
        return info;
    }

    // ******************
    // IOFMessageListener
    // ******************

    @Override
    public String getName() {
        return MODULE_NAME;
    }

    @Override
    public boolean isCallbackOrderingPrereq(OFType type, String name) {
        return ((type == OFType.PACKET_IN || type == OFType.FLOW_MOD)
                && name.equals("topology"));
    }

    @Override
    public boolean isCallbackOrderingPostreq(OFType type, String name) {
        return false;
    }

    @Override
    public Command receive(IOFSwitch sw, OFMessage msg,
                           FloodlightContext cntx) {
        switch (msg.getType()) {
            case PACKET_IN:
                cntIncoming.updateCounterNoFlush();
                return this.processPacketInMessage(sw,
                                                   (OFPacketIn) msg, cntx);
            default:
                break;
        }
        return Command.CONTINUE;
    }

    // ***************
    // IFlowReconcileListener
    // ***************
    @Override
    public Command reconcileFlows(ArrayList<OFMatchReconcile> ofmRcList) {
        ListIterator<OFMatchReconcile> iter = ofmRcList.listIterator();
        while (iter.hasNext()) {
            OFMatchReconcile ofm = iter.next();

            // Remove the STOPPed flow.
            if (Command.STOP == reconcileFlow(ofm)) {
                iter.remove();
            }
        }

        if (ofmRcList.size() > 0) {
            return Command.CONTINUE;
        } else {
            return Command.STOP;
        }
    }

    protected Command reconcileFlow(OFMatchReconcile ofm) {
        cntReconcileRequest.updateCounterNoFlush();
        // Extract source entity information
        Entity srcEntity =
                getEntityFromFlowMod(ofm.ofmWithSwDpid, true);
        if (srcEntity == null) {
            cntReconcileNoSource.updateCounterNoFlush();
            return Command.STOP;
       }

        // Find the device by source entity
        Device srcDevice = findDeviceByEntity(srcEntity);
        if (srcDevice == null)  {
            cntReconcileNoSource.updateCounterNoFlush();
            return Command.STOP;
        }
        // Store the source device in the context
        fcStore.put(ofm.cntx, CONTEXT_SRC_DEVICE, srcDevice);

        // Find the device matching the destination from the entity
        // classes of the source.
        Entity dstEntity = getEntityFromFlowMod(ofm.ofmWithSwDpid, false);
        Device dstDevice = null;
        if (dstEntity != null) {
            dstDevice = findDestByEntity(srcDevice.getEntityClass(), dstEntity);
            if (dstDevice != null)
                fcStore.put(ofm.cntx, CONTEXT_DST_DEVICE, dstDevice);
            else
                cntReconcileNoDest.updateCounterNoFlush();
        } else {
            cntReconcileNoDest.updateCounterNoFlush();
        }
        if (logger.isTraceEnabled()) {
            logger.trace("Reconciling flow: match={}, srcEntity={}, srcDev={}, "
                         + "dstEntity={}, dstDev={}",
                         new Object[] {ofm.ofmWithSwDpid.getOfMatch(),
                                       srcEntity, srcDevice,
                                       dstEntity, dstDevice } );
        }
        return Command.CONTINUE;
    }

    // *****************
    // IFloodlightModule
    // *****************

    @Override
    public Collection<Class<? extends IFloodlightService>> getModuleServices() {
        Collection<Class<? extends IFloodlightService>> l =
                new ArrayList<Class<? extends IFloodlightService>>();
        l.add(IDeviceService.class);
        return l;
    }

    @Override
    public Map<Class<? extends IFloodlightService>, IFloodlightService>
    getServiceImpls() {
        Map<Class<? extends IFloodlightService>,
        IFloodlightService> m =
        new HashMap<Class<? extends IFloodlightService>,
        IFloodlightService>();
        // We are the class that implements the service
        m.put(IDeviceService.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(ITopologyService.class);
        l.add(IRestApiService.class);
        l.add(IThreadPoolService.class);
        l.add(IFlowReconcileService.class);
        l.add(IEntityClassifierService.class);
        l.add(ISyncService.class);
        return l;
    }

    @Override
    public void init(FloodlightModuleContext fmc) throws FloodlightModuleException {
        this.perClassIndices =
                new HashSet<EnumSet<DeviceField>>();
        addIndex(true, EnumSet.of(DeviceField.IPV4));

        this.deviceListeners = new ListenerDispatcher<String, IDeviceListener>();
        this.suppressAPs = Collections.newSetFromMap(
                               new ConcurrentHashMap<SwitchPort, Boolean>());

        this.floodlightProvider =
                fmc.getServiceImpl(IFloodlightProviderService.class);
        this.storageSource =
                fmc.getServiceImpl(IStorageSourceService.class);
        this.topology =
                fmc.getServiceImpl(ITopologyService.class);
        this.restApi = fmc.getServiceImpl(IRestApiService.class);
        this.threadPool = fmc.getServiceImpl(IThreadPoolService.class);
        this.flowReconcileMgr = fmc.getServiceImpl(IFlowReconcileService.class);
        this.flowReconcileEngine = fmc.getServiceImpl(IFlowReconcileEngineService.class);
        this.entityClassifier = fmc.getServiceImpl(IEntityClassifierService.class);
        this.debugCounters = fmc.getServiceImpl(IDebugCounterService.class);
        this.debugEvents = fmc.getServiceImpl(IDebugEventService.class);
        this.syncService = fmc.getServiceImpl(ISyncService.class);
        this.deviceSyncManager = new DeviceSyncManager();
        this.haListenerDelegate = new HAListenerDelegate();
        registerDeviceManagerDebugCounters();
        registerDeviceManagerDebugEvents();
        this.addListener(new DeviceDebugEventLogger());
    }

    private void registerDeviceManagerDebugEvents() throws FloodlightModuleException {
        if (debugEvents == null) {
            debugEvents = new NullDebugEvent();
        }
        try {
            evDevice =
                debugEvents.registerEvent(PACKAGE, "hostevent",
                                          "Host added, removed, updated, or moved",
                                          EventType.ALWAYS_LOG, DeviceEvent.class, 500);
        } catch (MaxEventsRegistered e) {
            throw new FloodlightModuleException("Max events registered", e);
        }
    }

    @Override
    public void startUp(FloodlightModuleContext fmc)
            throws FloodlightModuleException {
        isMaster = (floodlightProvider.getRole() == Role.MASTER);
        primaryIndex = new DeviceUniqueIndex(entityClassifier.getKeyFields());
        secondaryIndexMap = new HashMap<EnumSet<DeviceField>, DeviceIndex>();

        deviceMap = new ConcurrentHashMap<Long, Device>();
        classStateMap =
                new ConcurrentHashMap<String, ClassState>();
        apComparator = new AttachmentPointComparator();

        floodlightProvider.addOFMessageListener(OFType.PACKET_IN, this);
        floodlightProvider.addHAListener(this.haListenerDelegate);
        if (topology != null)
            topology.addListener(this);
        flowReconcileMgr.addFlowReconcileListener(this);
        entityClassifier.addListener(this);

        ScheduledExecutorService ses = threadPool.getScheduledExecutor();
        Runnable ecr = new Runnable() {
            @Override
            public void run() {
                cleanupEntities();
                entityCleanupTask.reschedule(ENTITY_CLEANUP_INTERVAL,
                                             TimeUnit.SECONDS);
            }
        };
        entityCleanupTask = new SingletonTask(ses, ecr);
        entityCleanupTask.reschedule(ENTITY_CLEANUP_INTERVAL,
                                     TimeUnit.SECONDS);

        Runnable consolidateStoreRunner = new Runnable() {
            @Override
            public void run() {
                deviceSyncManager.consolidateStore();
                storeConsolidateTask.reschedule(syncStoreConsolidateIntervalMs,
                                                TimeUnit.MILLISECONDS);
            }
        };
        storeConsolidateTask = new SingletonTask(ses, consolidateStoreRunner);
        if (isMaster)
            storeConsolidateTask.reschedule(syncStoreConsolidateIntervalMs,
                                            TimeUnit.MILLISECONDS);


        if (restApi != null) {
            restApi.addRestletRoutable(new DeviceRoutable());
        } else {
            logger.debug("Could not instantiate REST API");
        }

        try {
            this.syncService.registerStore(DEVICE_SYNC_STORE_NAME, Scope.LOCAL);
            this.storeClient = this.syncService
                    .getStoreClient(DEVICE_SYNC_STORE_NAME,
                                    String.class,
                                    DeviceSyncRepresentation.class);
        } catch (SyncException e) {
            throw new FloodlightModuleException("Error while setting up sync service", e);
        }
        floodlightProvider.addInfoProvider("summary", this);
    }

    private void registerDeviceManagerDebugCounters() throws FloodlightModuleException {
        if (debugCounters == null) {
            logger.error("Debug Counter Service not found.");
            debugCounters = new NullDebugCounter();
        }
        try {
            cntIncoming = debugCounters.registerCounter(PACKAGE, "incoming",
                "All incoming packets seen by this module", CounterType.ALWAYS_COUNT);
            cntReconcileRequest = debugCounters.registerCounter(PACKAGE,
                "reconcile-request",
                "Number of flows that have been received for reconciliation by " +
                "this module",
                CounterType.ALWAYS_COUNT);
            cntReconcileNoSource = debugCounters.registerCounter(PACKAGE,
                "reconcile-no-source-device",
                "Number of flow reconcile events that failed because no source " +
                "device could be identified",
                CounterType.ALWAYS_COUNT, IDebugCounterService.CTR_MDATA_WARN); // is this really a IDebugCounterService.CTR_MDATA_WARNing
            cntReconcileNoDest = debugCounters.registerCounter(PACKAGE,
                "reconcile-no-dest-device",
                "Number of flow reconcile events that failed because no " +
                "destination device could be identified",
                CounterType.ALWAYS_COUNT, IDebugCounterService.CTR_MDATA_WARN); // is this really a IDebugCounterService.CTR_MDATA_WARNing
            cntInvalidSource = debugCounters.registerCounter(PACKAGE,
                "invalid-source",
                "Number of packetIns that were discarded because the source " +
                "MAC was invalid (broadcast, multicast, or zero)",
                CounterType.ALWAYS_COUNT, IDebugCounterService.CTR_MDATA_WARN);
            cntNoSource = debugCounters.registerCounter(PACKAGE, "no-source-device",
                 "Number of packetIns that were discarded because the " +
                 "could not identify a source device. This can happen if a " +
                 "packet is not allowed, appears on an illegal port, does not " +
                 "have a valid address space, etc.",
                 CounterType.ALWAYS_COUNT, IDebugCounterService.CTR_MDATA_WARN);
            cntInvalidDest = debugCounters.registerCounter(PACKAGE,
                "invalid-dest",
                "Number of packetIns that were discarded because the dest " +
                "MAC was invalid (zero)",
                 CounterType.ALWAYS_COUNT, IDebugCounterService.CTR_MDATA_WARN);
            cntNoDest = debugCounters.registerCounter(PACKAGE, "no-dest-device",
                 "Number of packetIns that did not have an associated " +
                 "destination device. E.g., because the destination MAC is " +
                 "broadcast/multicast or is not yet known to the controller.",
                 CounterType.ALWAYS_COUNT);
            cntDhcpClientNameSnooped = debugCounters.registerCounter(PACKAGE,
                 "dhcp-client-name-snooped",
                 "Number of times a DHCP client name was snooped from a " +
                 "packetIn.",
                 CounterType.ALWAYS_COUNT);
            cntDeviceOnInternalPortNotLearned = debugCounters.registerCounter(
                 PACKAGE,
                 "device-on-internal-port-not-learned",
                 "Number of times packetIn was received on an internal port and" +
                 "no source device is known for the source MAC. The packetIn is " +
                 "discarded.",
                 CounterType.ALWAYS_COUNT, IDebugCounterService.CTR_MDATA_WARN);
            cntPacketNotAllowed = debugCounters.registerCounter(PACKAGE,
                 "packet-not-allowed",
                 "Number of times a packetIn was not allowed due to spoofing " +
                 "protection configuration.",
                 CounterType.ALWAYS_COUNT, IDebugCounterService.CTR_MDATA_WARN); // is this really a IDebugCounterService.CTR_MDATA_WARNing?
            cntNewDevice = debugCounters.registerCounter(PACKAGE, "new-device",
                 "Number of times a new device was learned",
                 CounterType.ALWAYS_COUNT);
            cntPacketOnInternalPortForKnownDevice = debugCounters.registerCounter(
                 PACKAGE,
                 "packet-on-internal-port-for-known-device",
                 "Number of times a packetIn was received on an internal port " +
                 "for a known device.",
                 CounterType.ALWAYS_COUNT);
            cntNewEntity = debugCounters.registerCounter(PACKAGE, "new-entity",
                 "Number of times a new entity was learned for an existing device",
                 CounterType.ALWAYS_COUNT);
            cntDeviceChanged = debugCounters.registerCounter(PACKAGE, "device-changed",
                 "Number of times device properties have changed",
                 CounterType.ALWAYS_COUNT);
            cntDeviceMoved = debugCounters.registerCounter(PACKAGE, "device-moved",
                 "Number of times devices have moved",
                 CounterType.ALWAYS_COUNT);
            cntCleanupEntitiesRuns = debugCounters.registerCounter(PACKAGE,
                 "cleanup-entities-runs",
                 "Number of times the entity cleanup task has been run",
                 CounterType.ALWAYS_COUNT);
            cntEntityRemovedTimeout = debugCounters.registerCounter(PACKAGE,
                 "entity-removed-timeout",
                 "Number of times entities have been removed due to timeout " +
                 "(entity has been inactive for " + ENTITY_TIMEOUT/1000 + "s)",
                 CounterType.ALWAYS_COUNT);
            cntDeviceDeleted = debugCounters.registerCounter(PACKAGE, "device-deleted",
                 "Number of devices that have been removed due to inactivity",
                 CounterType.ALWAYS_COUNT);
            cntDeviceReclassifyDelete = debugCounters.registerCounter(PACKAGE,
                 "device-reclassify-delete",
                 "Number of devices that required reclassification and have been " +
                 "temporarily delete for reclassification",
                 CounterType.ALWAYS_COUNT);
            cntDeviceStrored = debugCounters.registerCounter(PACKAGE, "device-stored",
                 "Number of device entries written or updated to the sync store",
                 CounterType.ALWAYS_COUNT);
            cntDeviceStoreThrottled = debugCounters.registerCounter(PACKAGE,
                 "device-store-throttled",
                 "Number of times a device update to the sync store was " +
                 "requested but not performed because the same device entities " +
                 "have recently been updated already",
                 CounterType.ALWAYS_COUNT);
            cntDeviceRemovedFromStore = debugCounters.registerCounter(PACKAGE,
                 "device-removed-from-store",
                 "Number of devices that were removed from the sync store " +
                 "because the local controller removed the device due to " +
                 "inactivity",
                 CounterType.ALWAYS_COUNT);
            cntSyncException = debugCounters.registerCounter(PACKAGE, "sync-exception",
                 "Number of times an operation on the sync store resulted in " +
                 "sync exception",
                 CounterType.ALWAYS_COUNT, IDebugCounterService.CTR_MDATA_WARN); // it this an error?
            cntDevicesFromStore = debugCounters.registerCounter(PACKAGE,
                 "devices-from-store",
                 "Number of devices that were read from the sync store after " +
                 "the local controller transitioned from SLAVE to MASTER",
                 CounterType.ALWAYS_COUNT);
            cntConsolidateStoreRuns = debugCounters.registerCounter(PACKAGE,
                 "consolidate-store-runs",
                 "Number of times the task to consolidate entries in the " +
                 "store witch live known devices has been run",
                 CounterType.ALWAYS_COUNT);
            cntConsolidateStoreDevicesRemoved = debugCounters.registerCounter(PACKAGE,
                 "consolidate-store-devices-removed",
                 "Number of times a device has been removed from the sync " +
                 "store because no corresponding live device is known. " +
                 "This indicates a remote controller still writing device " +
                 "entries despite the local controller being MASTER or an " +
                 "incosistent store update from the local controller.",
                 CounterType.ALWAYS_COUNT, IDebugCounterService.CTR_MDATA_WARN);
            cntTransitionToMaster = debugCounters.registerCounter(PACKAGE,
                 "transition-to-master",
                 "Number of times this controller has transitioned from SLAVE " +
                 "to MASTER role. Will be 0 or 1.",
                 CounterType.ALWAYS_COUNT);
        } catch (CounterException e) {
            throw new FloodlightModuleException(e.getMessage());
        }
    }

    // ***************
    // IHAListener
    // ***************

    protected class HAListenerDelegate implements IHAListener {
        @Override
        public void transitionToMaster() {
            DeviceManagerImpl.this.isMaster = true;
            DeviceManagerImpl.this.deviceSyncManager.goToMaster();
        }

        @Override
        public void controllerNodeIPsChanged(
                Map<String, String> curControllerNodeIPs,
                Map<String, String> addedControllerNodeIPs,
                Map<String, String> removedControllerNodeIPs) {
            // no-op
        }

        @Override
        public String getName() {
            return DeviceManagerImpl.this.getName();
        }

        @Override
        public boolean isCallbackOrderingPrereq(HAListenerTypeMarker type,
                                                String name) {
            return ("topology".equals(name) ||
                    "bvsmanager".equals(name));
        }

        @Override
        public boolean isCallbackOrderingPostreq(HAListenerTypeMarker type,
                                                 String name) {
            // TODO Auto-generated method stub
            return false;
        }
    }


    // ****************
    // Internal methods
    // ****************

    protected Command processPacketInMessage(IOFSwitch sw, OFPacketIn pi,
                                             FloodlightContext cntx) {
        Ethernet eth =
                IFloodlightProviderService.bcStore.
                get(cntx,IFloodlightProviderService.CONTEXT_PI_PAYLOAD);

        // Extract source entity information
        Entity srcEntity =
                getSourceEntityFromPacket(eth, sw.getId(), pi.getInPort());
        if (srcEntity == null) {
            cntInvalidSource.updateCounterNoFlush();
            return Command.STOP;
        }

        // Learn from ARP packet for special VRRP settings.
        // In VRRP settings, the source MAC address and sender MAC
        // addresses can be different.  In such cases, we need to learn
        // the IP to MAC mapping of the VRRP IP address.  The source
        // entity will not have that information.  Hence, a separate call
        // to learn devices in such cases.
        learnDeviceFromArpResponseData(eth, sw.getId(), pi.getInPort());

        // Learn/lookup device information
        Device srcDevice = learnDeviceByEntity(srcEntity);
        if (srcDevice == null) {
            cntNoSource.updateCounterNoFlush();
            return Command.STOP;
        }

        // Store the source device in the context
        fcStore.put(cntx, CONTEXT_SRC_DEVICE, srcDevice);

        // Find the device matching the destination from the entity
        // classes of the source.
        if (eth.getDestinationMAC().toLong() == 0) {
            cntInvalidDest.updateCounterNoFlush();
            return Command.STOP;
        }
        Entity dstEntity = getDestEntityFromPacket(eth);
        Device dstDevice = null;
        if (dstEntity != null) {
            dstDevice =
                    findDestByEntity(srcDevice.getEntityClass(), dstEntity);
            if (dstDevice != null)
                fcStore.put(cntx, CONTEXT_DST_DEVICE, dstDevice);
            else
                cntNoDest.updateCounterNoFlush();
        } else {
            cntNoDest.updateCounterNoFlush();
        }

       if (logger.isTraceEnabled()) {
           logger.trace("Received PI: {} on switch {}, port {} *** eth={}" +
                        " *** srcDev={} *** dstDev={} *** ",
                        new Object[] { pi, sw.getStringId(), pi.getInPort(), eth,
                        srcDevice, dstDevice });
       }

        snoopDHCPClientName(eth, srcDevice);

        return Command.CONTINUE;
    }

    /**
     * Snoop and record client-provided host name from DHCP requests
     * @param eth
     * @param srcDevice
     */
    private void snoopDHCPClientName(Ethernet eth, Device srcDevice) {
        if (! (eth.getPayload() instanceof IPv4) )
            return;
        IPv4 ipv4 = (IPv4) eth.getPayload();
        if (! (ipv4.getPayload() instanceof UDP) )
            return;
        UDP udp = (UDP) ipv4.getPayload();
        if (!(udp.getPayload() instanceof DHCP))
            return;
        DHCP dhcp = (DHCP) udp.getPayload();
        byte opcode = dhcp.getOpCode();
        if (opcode == DHCP.OPCODE_REQUEST) {
            DHCPOption dhcpOption = dhcp.getOption(
                    DHCPOptionCode.OptionCode_Hostname);
            if (dhcpOption != null) {
                cntDhcpClientNameSnooped.updateCounterNoFlush();
                srcDevice.dhcpClientName = new String(dhcpOption.getData());
            }
        }
    }

    /**
     * Check whether the given attachment point is valid given the current
     * topology
     * @param switchDPID the DPID
     * @param switchPort the port
     * @return true if it's a valid attachment point
     */
    public boolean isValidAttachmentPoint(long switchDPID,
                                             int switchPort) {
        if (topology.isAttachmentPointPort(switchDPID,
                                           (short)switchPort) == false)
            return false;

        if (suppressAPs.contains(new SwitchPort(switchDPID, switchPort)))
            return false;

        return true;
    }

    /**
     * Get sender IP address from packet if the packet is an ARP
     * packet and if the source MAC address matches the ARP packets
     * sender MAC address.
     * @param eth
     * @param dlAddr
     * @return
     */
    private int getSrcNwAddr(Ethernet eth, long dlAddr) {
        if (eth.getPayload() instanceof ARP) {
            ARP arp = (ARP) eth.getPayload();
            if ((arp.getProtocolType() == ARP.PROTO_TYPE_IP) &&
                    (Ethernet.toLong(arp.getSenderHardwareAddress()) == dlAddr)) {
                return IPv4.toIPv4Address(arp.getSenderProtocolAddress());
            }
        }
        return 0;
    }

    /**
     * Parse an entity from an {@link Ethernet} packet.
     * @param eth the packet to parse
     * @param sw the switch on which the packet arrived
     * @param pi the original packetin
     * @return the entity from the packet
     */
    protected Entity getSourceEntityFromPacket(Ethernet eth,
                                             long swdpid,
                                             int port) {
        byte[] dlAddrArr = eth.getSourceMACAddress();
        long dlAddr = Ethernet.toLong(dlAddrArr);

        // Ignore broadcast/multicast source
        if ((dlAddrArr[0] & 0x1) != 0)
            return null;
        // Ignore 0 source mac
        if (dlAddr == 0)
            return null;

        short vlan = eth.getVlanID();
        int nwSrc = getSrcNwAddr(eth, dlAddr);
        return new Entity(dlAddr,
                          ((vlan >= 0) ? vlan : null),
                          ((nwSrc != 0) ? nwSrc : null),
                          swdpid,
                          port,
                          new Date());
    }

    /**
     * Learn device from ARP data in scenarios where the
     * Ethernet source MAC is different from the sender hardware
     * address in ARP data.
     */
    protected void learnDeviceFromArpResponseData(Ethernet eth,
                                            long swdpid,
                                            int port) {

        if (!(eth.getPayload() instanceof ARP)) return;
        ARP arp = (ARP) eth.getPayload();

        byte[] dlAddrArr = eth.getSourceMACAddress();
        long dlAddr = Ethernet.toLong(dlAddrArr);

        byte[] senderHardwareAddr = arp.getSenderHardwareAddress();
        long senderAddr = Ethernet.toLong(senderHardwareAddr);

        if (dlAddr == senderAddr) return;

        // Ignore broadcast/multicast source
        if ((senderHardwareAddr[0] & 0x1) != 0)
            return;
        // Ignore zero sender mac
        if (senderAddr == 0)
            return;

        short vlan = eth.getVlanID();
        int nwSrc = IPv4.toIPv4Address(arp.getSenderProtocolAddress());

        Entity e =  new Entity(senderAddr,
                ((vlan >= 0) ? vlan : null),
                ((nwSrc != 0) ? nwSrc : null),
                swdpid,
                port,
                new Date());

        learnDeviceByEntity(e);
    }

    /**
     * Get a (partial) entity for the destination from the packet.
     * @param eth
     * @return
     */
    protected Entity getDestEntityFromPacket(Ethernet eth) {
        byte[] dlAddrArr = eth.getDestinationMACAddress();
        long dlAddr = Ethernet.toLong(dlAddrArr);
        short vlan = eth.getVlanID();
        int nwDst = 0;

        // Ignore broadcast/multicast destination
        if ((dlAddrArr[0] & 0x1) != 0)
            return null;
        // Ignore zero dest mac
        if (dlAddr == 0)
            return null;

        if (eth.getPayload() instanceof IPv4) {
            IPv4 ipv4 = (IPv4) eth.getPayload();
            nwDst = ipv4.getDestinationAddress();
        }

        return new Entity(dlAddr,
                          ((vlan >= 0) ? vlan : null),
                          ((nwDst != 0) ? nwDst : null),
                          null,
                          null,
                          null);
    }

    /**
     * Parse an entity from an OFMatchWithSwDpid.
     * @param ofmWithSwDpid
     * @return the entity from the packet
     */
    private Entity getEntityFromFlowMod(OFMatchWithSwDpid ofmWithSwDpid,
                boolean isSource) {
        byte[] dlAddrArr = ofmWithSwDpid.getOfMatch().getDataLayerSource();
        int nwSrc = ofmWithSwDpid.getOfMatch().getNetworkSource();
        if (!isSource) {
            dlAddrArr = ofmWithSwDpid.getOfMatch().getDataLayerDestination();
            nwSrc = ofmWithSwDpid.getOfMatch().getNetworkDestination();
        }

        long dlAddr = Ethernet.toLong(dlAddrArr);

        // Ignore broadcast/multicast source
        if ((dlAddrArr[0] & 0x1) != 0)
            return null;

        Long swDpid = null;
        Short inPort = null;

        if (isSource) {
            swDpid = ofmWithSwDpid.getSwitchDataPathId();
            inPort = ofmWithSwDpid.getOfMatch().getInputPort();
        }

        /**for the new flow cache design, the flow mods retrived are not always
         * from the source, learn AP should be disabled --meiyang*/
        boolean learnap = false;
        /**
         * if (swDpid == null ||
            inPort == null ||
            !isValidAttachmentPoint(swDpid, inPort)) {
            // If this is an internal port or we otherwise don't want
            // to learn on these ports.  In the future, we should
            // handle this case by labeling flows with something that
            // will give us the entity class.  For now, we'll do our
            // best assuming attachment point information isn't used
            // as a key field.
            learnap = false;
        }
        */

        short vlan = ofmWithSwDpid.getOfMatch().getDataLayerVirtualLan();
        return new Entity(dlAddr,
                          ((vlan >= 0) ? vlan : null),
                          ((nwSrc != 0) ? nwSrc : null),
                          (learnap ? swDpid : null),
                          (learnap ? (int)inPort : null),
                          new Date());
    }

    /**
     * Look up a {@link Device} based on the provided {@link Entity}. We first
     * check the primary index. If we do not find an entry there we classify
     * the device into its IEntityClass and query the classIndex.
     * This implies that all key field of the current IEntityClassifier must
     * be present in the entity for the lookup to succeed!
     * @param entity the entity to search for
     * @return The {@link Device} object if found
     */
    protected Device findDeviceByEntity(Entity entity) {
        // Look up the fully-qualified entity to see if it already
        // exists in the primary entity index.
        Long deviceKey = primaryIndex.findByEntity(entity);
        IEntityClass entityClass = null;

        if (deviceKey == null) {
            // If the entity does not exist in the primary entity index,
            // use the entity classifier for find the classes for the
            // entity. Look up the entity in the returned class'
            // class entity index.
            entityClass = entityClassifier.classifyEntity(entity);
            if (entityClass == null) {
                return null;
            }
            ClassState classState = getClassState(entityClass);

            if (classState.classIndex != null) {
                deviceKey =
                        classState.classIndex.findByEntity(entity);
            }
        }
        if (deviceKey == null) return null;
        return deviceMap.get(deviceKey);
    }

    /**
     * Get a destination device using entity fields that corresponds with
     * the given source device.  The source device is important since
     * there could be ambiguity in the destination device without the
     * attachment point information.
     * @param reference  the source device's entity class.
     *                   The returned destination will be
     *                   in the same entity class as the source.
     * @param dstEntity  the entity to look up
     * @return an {@link Device} or null if no device is found.
     */
    protected Device findDestByEntity(IEntityClass reference,
                                      Entity dstEntity) {

        // Look  up the fully-qualified entity to see if it
        // exists in the primary entity index
        Long deviceKey = primaryIndex.findByEntity(dstEntity);

        if (deviceKey == null) {
            // This could happen because:
            // 1) no destination known, or a broadcast destination
            // 2) if we have attachment point key fields since
            // attachment point information isn't available for
            // destination devices.
            // For the second case, we'll need to match up the
            // destination device with the class of the source
            // device.
            ClassState classState = getClassState(reference);
            if (classState.classIndex == null) {
                return null;
            }
            deviceKey = classState.classIndex.findByEntity(dstEntity);
        }
        if (deviceKey == null) return null;
        return deviceMap.get(deviceKey);
    }

    /**
     * Look up a {@link Device} within a particular entity class based on
     * the provided {@link Entity}.
     * @param clazz the entity class to search for the entity
     * @param entity the entity to search for
     * @return The {@link Device} object if found
    private Device findDeviceInClassByEntity(IEntityClass clazz,
                                               Entity entity) {
        // XXX - TODO
        throw new UnsupportedOperationException();
    }
     */

    /**
     * Look up a {@link Device} based on the provided {@link Entity}.  Also
     * learns based on the new entity, and will update existing devices as
     * required.
     *
     * @param entity the {@link Entity}
     * @return The {@link Device} object if found
     */
    protected Device learnDeviceByEntity(Entity entity) {
        ArrayList<Long> deleteQueue = null;
        LinkedList<DeviceUpdate> deviceUpdates = null;
        Device device = null;

        // we may need to restart the learning process if we detect
        // concurrent modification.  Note that we ensure that at least
        // one thread should always succeed so we don't get into infinite
        // starvation loops
        while (true) {
            deviceUpdates = null;

            // Look up the fully-qualified entity to see if it already
            // exists in the primary entity index.
            Long deviceKey = primaryIndex.findByEntity(entity);
            IEntityClass entityClass = null;

            if (deviceKey == null) {
                // If the entity does not exist in the primary entity index,
                // use the entity classifier for find the classes for the
                // entity. Look up the entity in the returned class'
                // class entity index.
                entityClass = entityClassifier.classifyEntity(entity);
                if (entityClass == null) {
                    // could not classify entity. No device
                    device = null;
                    break;
                }
                ClassState classState = getClassState(entityClass);

                if (classState.classIndex != null) {
                    deviceKey =
                            classState.classIndex.findByEntity(entity);
                }
            }
            if (deviceKey != null) {
                // If the primary or secondary index contains the entity
                // use resulting device key to look up the device in the
                // device map, and use the referenced Device below.
                device = deviceMap.get(deviceKey);
                if (device == null) {
                    // This can happen due to concurrent modification
                    if (logger.isDebugEnabled()) {
                        logger.debug("No device for deviceKey {} while "
                                     + "while processing entity {}",
                                     deviceKey, entity);
                    }
                    // if so, then try again till we don't even get the device key
                    // and so we recreate the device
                    continue;
                }
            } else {
                // If the secondary index does not contain the entity,
                // create a new Device object containing the entity, and
                // generate a new device ID if the the entity is on an
                // attachment point port. Otherwise ignore.
                if (entity.hasSwitchPort() &&
                        !topology.isAttachmentPointPort(entity.getSwitchDPID(),
                                                 entity.getSwitchPort().shortValue())) {
                    cntDeviceOnInternalPortNotLearned.updateCounterNoFlush();
                    if (logger.isDebugEnabled()) {
                        logger.debug("Not learning new device on internal"
                                     + " link: {}", entity);
                    }
                    device = null;
                    break;
                }
                // Before we create the new device also check if
                // the entity is allowed (e.g., for spoofing protection)
                if (!isEntityAllowed(entity, entityClass)) {
                    cntPacketNotAllowed.updateCounterNoFlush();
                    if (logger.isDebugEnabled()) {
                        logger.debug("PacketIn is not allowed {} {}",
                                    entityClass.getName(), entity);
                    }
                    device = null;
                    break;
                }
                deviceKey = deviceKeyCounter.getAndIncrement();
                device = allocateDevice(deviceKey, entity, entityClass);


                // Add the new device to the primary map with a simple put
                deviceMap.put(deviceKey, device);

                // update indices
                if (!updateIndices(device, deviceKey)) {
                    if (deleteQueue == null)
                        deleteQueue = new ArrayList<Long>();
                    deleteQueue.add(deviceKey);
                    continue;
                }

                updateSecondaryIndices(entity, entityClass, deviceKey);

                // We need to count and log here. If we log earlier we could
                // hit a concurrent modification and restart the dev creation
                // and potentially count the device twice.
                cntNewDevice.updateCounterNoFlush();
                if (logger.isDebugEnabled()) {
                    logger.debug("New device created: {} deviceKey={}, entity={}",
                                 new Object[]{device, deviceKey, entity});
                }
                // generate new device update
                deviceUpdates =
                        updateUpdates(deviceUpdates,
                                      new DeviceUpdate(device, ADD, null));

                break;
            }
            // if it gets here, we have a pre-existing Device for this Entity
            if (!isEntityAllowed(entity, device.getEntityClass())) {
                cntPacketNotAllowed.updateCounterNoFlush();
                if (logger.isDebugEnabled()) {
                    logger.info("PacketIn is not allowed {} {}",
                                device.getEntityClass().getName(), entity);
                }
                return null;
            }
            // If this is not an attachment point port we don't learn the new entity
            // and don't update indexes. But we do allow the device to continue up
            // the chain.
            if (entity.hasSwitchPort() &&
                    !topology.isAttachmentPointPort(entity.getSwitchDPID(),
                                                 entity.getSwitchPort().shortValue())) {
                cntPacketOnInternalPortForKnownDevice.updateCounterNoFlush();
                break;
            }
            int entityindex = -1;
            if ((entityindex = device.entityIndex(entity)) >= 0) {
                // Entity already exists
                // update timestamp on the found entity
                Date lastSeen = entity.getLastSeenTimestamp();
                if (lastSeen == null) {
                    lastSeen = new Date();
                    entity.setLastSeenTimestamp(lastSeen);
                }
                device.entities[entityindex].setLastSeenTimestamp(lastSeen);
                // we break the loop after checking for changes to the AP
            } else {
                // New entity for this device
                // compute the insertion point for the entity.
                // see Arrays.binarySearch()
                entityindex = -(entityindex + 1);
                Device newDevice = allocateDevice(device, entity, entityindex);

                // generate updates
                EnumSet<DeviceField> changedFields =
                        findChangedFields(device, entity);

                // update the device map with a replace call
                boolean res = deviceMap.replace(deviceKey, device, newDevice);
                // If replace returns false, restart the process from the
                // beginning (this implies another thread concurrently
                // modified this Device).
                if (!res)
                    continue;

                device = newDevice;
                // update indices
                if (!updateIndices(device, deviceKey)) {
                    continue;
                }
                updateSecondaryIndices(entity,
                                       device.getEntityClass(),
                                       deviceKey);

                // We need to count here after all the possible "continue"
                // statements in this branch
                cntNewEntity.updateCounterNoFlush();
                if (changedFields.size() > 0) {
                    cntDeviceChanged.updateCounterNoFlush();
                    deviceUpdates =
                    updateUpdates(deviceUpdates,
                                  new DeviceUpdate(newDevice, CHANGE,
                                                   changedFields));
                }
                // we break the loop after checking for changed AP
            }
            // Update attachment point (will only be hit if the device
            // already existed and no concurrent modification)
            if (entity.hasSwitchPort()) {
                boolean moved =
                        device.updateAttachmentPoint(entity.getSwitchDPID(),
                                entity.getSwitchPort().shortValue(),
                                entity.getLastSeenTimestamp().getTime());
                // TODO: use update mechanism instead of sending the
                // notification directly
                if (moved) {
                    // we count device moved events in sendDeviceMovedNotification()
                    sendDeviceMovedNotification(device);
                    if (logger.isTraceEnabled()) {
                        logger.trace("Device moved: attachment points {}," +
                                "entities {}", device.attachmentPoints,
                                device.entities);
                    }
                } else {
                    if (logger.isTraceEnabled()) {
                        logger.trace("Device attachment point updated: " +
                                     "attachment points {}," +
                                     "entities {}", device.attachmentPoints,
                                     device.entities);
                    }
                }
            }
            break;
        }

        if (deleteQueue != null) {
            for (Long l : deleteQueue) {
                Device dev = deviceMap.get(l);
                this.deleteDevice(dev);
            }
        }

        processUpdates(deviceUpdates);
        deviceSyncManager.storeDeviceThrottled(device);

        return device;
    }

    protected boolean isEntityAllowed(Entity entity, IEntityClass entityClass) {
        return true;
    }





    protected EnumSet<DeviceField> findChangedFields(Device device,
                                                     Entity newEntity) {
        EnumSet<DeviceField> changedFields =
                EnumSet.of(DeviceField.IPV4,
                           DeviceField.VLAN,
                           DeviceField.SWITCH);

        if (newEntity.getIpv4Address() == null)
            changedFields.remove(DeviceField.IPV4);
        if (newEntity.getVlan() == null)
            changedFields.remove(DeviceField.VLAN);
        if (newEntity.getSwitchDPID() == null ||
                newEntity.getSwitchPort() == null)
            changedFields.remove(DeviceField.SWITCH);

        if (changedFields.size() == 0) return changedFields;

        for (Entity entity : device.getEntities()) {
            if (newEntity.getIpv4Address() == null ||
                    (entity.getIpv4Address() != null &&
                    entity.getIpv4Address().equals(newEntity.getIpv4Address())))
                changedFields.remove(DeviceField.IPV4);
            if (newEntity.getVlan() == null ||
                    (entity.getVlan() != null &&
                    entity.getVlan().equals(newEntity.getVlan())))
                changedFields.remove(DeviceField.VLAN);
            if (newEntity.getSwitchDPID() == null ||
                    newEntity.getSwitchPort() == null ||
                    (entity.getSwitchDPID() != null &&
                    entity.getSwitchPort() != null &&
                    entity.getSwitchDPID().equals(newEntity.getSwitchDPID()) &&
                    entity.getSwitchPort().equals(newEntity.getSwitchPort())))
                changedFields.remove(DeviceField.SWITCH);
        }

        return changedFields;
    }

    /**
     * Send update notifications to listeners
     * @param updates the updates to process.
     */
    protected void processUpdates(Queue<DeviceUpdate> updates) {
        if (updates == null) return;
        DeviceUpdate update = null;
        while (null != (update = updates.poll())) {
            if (logger.isTraceEnabled()) {
                logger.trace("Dispatching device update: {}", update);
            }
            if (update.change == DeviceUpdate.Change.DELETE)
                deviceSyncManager.removeDevice(update.device);
            else
                deviceSyncManager.storeDevice(update.device);
            List<IDeviceListener> listeners = deviceListeners.getOrderedListeners();
            notifyListeners(listeners, update);
        }
    }

    protected void notifyListeners(List<IDeviceListener> listeners, DeviceUpdate update) {
        if (listeners == null) {
            return;
        }
        for (IDeviceListener listener : listeners) {
            switch (update.change) {
                case ADD:
                    listener.deviceAdded(update.device);
                    break;
                case DELETE:
                    listener.deviceRemoved(update.device);
                    break;
                case CHANGE:
                    for (DeviceField field : update.fieldsChanged) {
                        switch (field) {
                            case IPV4:
                                listener.deviceIPV4AddrChanged(update.device);
                                break;
                            case SWITCH:
                            case PORT:
                                //listener.deviceMoved(update.device);
                                break;
                            case VLAN:
                                listener.deviceVlanChanged(update.device);
                                break;
                            default:
                                logger.debug("Unknown device field changed {}",
                                            update.fieldsChanged.toString());
                                break;
                        }
                    }
                    break;
            }
        }
    }

    /**
     * Check if the entity e has all the keyFields set. Returns false if not
     * @param e entity to check
     * @param keyFields the key fields to check e against
     * @return
     */
    protected boolean allKeyFieldsPresent(Entity e, EnumSet<DeviceField> keyFields) {
        for (DeviceField f : keyFields) {
            switch (f) {
                case MAC:
                    // MAC address is always present
                    break;
                case IPV4:
                    if (e.ipv4Address == null) return false;
                    break;
                case SWITCH:
                    if (e.switchDPID == null) return false;
                    break;
                case PORT:
                    if (e.switchPort == null) return false;
                    break;
                case VLAN:
                    // FIXME: vlan==null is ambiguous: it can mean: not present
                    // or untagged
                    //if (e.vlan == null) return false;
                    break;
                default:
                    // we should never get here. unless somebody extended
                    // DeviceFields
                    throw new IllegalStateException();
            }
        }
        return true;
    }

    private LinkedList<DeviceUpdate>
    updateUpdates(LinkedList<DeviceUpdate> list, DeviceUpdate update) {
        if (update == null) return list;
        if (list == null)
            list = new LinkedList<DeviceUpdate>();
        list.add(update);

        return list;
    }

    /**
     * Get the secondary index for a class.  Will return null if the
     * secondary index was created concurrently in another thread.
     * @param clazz the class for the index
     * @return
     */
    private ClassState getClassState(IEntityClass clazz) {
        ClassState classState = classStateMap.get(clazz.getName());
        if (classState != null) return classState;

        classState = new ClassState(clazz);
        ClassState r = classStateMap.putIfAbsent(clazz.getName(), classState);
        if (r != null) {
            // concurrent add
            return r;
        }
        return classState;
    }

    /**
     * Update both the primary and class indices for the provided device.
     * If the update fails because of an concurrent update, will return false.
     * @param device the device to update
     * @param deviceKey the device key for the device
     * @return true if the update succeeded, false otherwise.
     */
    private boolean updateIndices(Device device, Long deviceKey) {
        if (!primaryIndex.updateIndex(device, deviceKey)) {
            return false;
        }
        IEntityClass entityClass = device.getEntityClass();
        ClassState classState = getClassState(entityClass);

        if (classState.classIndex != null) {
            if (!classState.classIndex.updateIndex(device,
                                                   deviceKey))
                return false;
        }
    return true;
    }

    /**
     * Update the secondary indices for the given entity and associated
     * entity classes
     * @param entity the entity to update
     * @param entityClass the entity class for the entity
     * @param deviceKey the device key to set up
     */
    private void updateSecondaryIndices(Entity entity,
                                        IEntityClass entityClass,
                                        Long deviceKey) {
        for (DeviceIndex index : secondaryIndexMap.values()) {
            index.updateIndex(entity, deviceKey);
        }
        ClassState state = getClassState(entityClass);
        for (DeviceIndex index : state.secondaryIndexMap.values()) {
            index.updateIndex(entity, deviceKey);
        }
    }

    /**
     * Clean up expired entities/devices
     */
    protected void cleanupEntities () {
        cntCleanupEntitiesRuns.updateCounterWithFlush();

        Calendar c = Calendar.getInstance();
        c.add(Calendar.MILLISECOND, -ENTITY_TIMEOUT);
        Date cutoff = c.getTime();

        ArrayList<Entity> toRemove = new ArrayList<Entity>();
        ArrayList<Entity> toKeep = new ArrayList<Entity>();

        Iterator<Device> diter = deviceMap.values().iterator();
        LinkedList<DeviceUpdate> deviceUpdates =
                new LinkedList<DeviceUpdate>();

        while (diter.hasNext()) {
            Device d = diter.next();

            while (true) {
                deviceUpdates.clear();
                toRemove.clear();
                toKeep.clear();
                for (Entity e : d.getEntities()) {
                    if (e.getLastSeenTimestamp() != null &&
                         0 > e.getLastSeenTimestamp().compareTo(cutoff)) {
                        // individual entity needs to be removed
                        toRemove.add(e);
                    } else {
                        toKeep.add(e);
                    }
                }
                if (toRemove.size() == 0) {
                    break;
                }

                cntEntityRemovedTimeout.updateCounterWithFlush();
                for (Entity e : toRemove) {
                    removeEntity(e, d.getEntityClass(), d.getDeviceKey(), toKeep);
                }

                if (toKeep.size() > 0) {
                    Device newDevice = allocateDevice(d.getDeviceKey(),
                                                      d.getDHCPClientName(),
                                                      d.oldAPs,
                                                      d.attachmentPoints,
                                                      toKeep,
                                                      d.getEntityClass());

                    EnumSet<DeviceField> changedFields =
                            EnumSet.noneOf(DeviceField.class);
                    for (Entity e : toRemove) {
                        changedFields.addAll(findChangedFields(newDevice, e));
                    }
                    DeviceUpdate update = null;
                    if (changedFields.size() > 0) {
                        update = new DeviceUpdate(d, CHANGE, changedFields);
                    }

                    if (!deviceMap.replace(newDevice.getDeviceKey(),
                                           d,
                                           newDevice)) {
                        // concurrent modification; try again
                        // need to use device that is the map now for the next
                        // iteration
                        d = deviceMap.get(d.getDeviceKey());
                        if (null != d)
                            continue;
                    }
                    if (update != null) {
                        // need to count after all possibly continue stmts in
                        // this branch
                        cntDeviceChanged.updateCounterWithFlush();
                        deviceUpdates.add(update);
                    }
                } else {
                    DeviceUpdate update = new DeviceUpdate(d, DELETE, null);
                    if (!deviceMap.remove(d.getDeviceKey(), d)) {
                        // concurrent modification; try again
                        // need to use device that is the map now for the next
                        // iteration
                        d = deviceMap.get(d.getDeviceKey());
                        if (null != d)
                            continue;
                        cntDeviceDeleted.updateCounterWithFlush();
                    }
                    deviceUpdates.add(update);
                }
                processUpdates(deviceUpdates);
                break;
            }
        }
        // Since cleanupEntities() is not called in the packet-in pipeline,
        // debugEvents need to be flushed explicitly
        debugEvents.flushEvents();
    }

    protected void removeEntity(Entity removed,
                              IEntityClass entityClass,
                              Long deviceKey,
                              Collection<Entity> others) {
        // Don't count in this method. This method CAN BE called to clean-up
        // after concurrent device adds/updates and thus counting here
        // is misleading
        for (DeviceIndex index : secondaryIndexMap.values()) {
            index.removeEntityIfNeeded(removed, deviceKey, others);
        }
        ClassState classState = getClassState(entityClass);
        for (DeviceIndex index : classState.secondaryIndexMap.values()) {
            index.removeEntityIfNeeded(removed, deviceKey, others);
        }

        primaryIndex.removeEntityIfNeeded(removed, deviceKey, others);

        if (classState.classIndex != null) {
            classState.classIndex.removeEntityIfNeeded(removed,
                                                       deviceKey,
                                                       others);
        }
    }

    /**
     * method to delete a given device, remove all entities first and then
     * finally delete the device itself.
     * @param device
     */
    protected void deleteDevice(Device device) {
        // Don't count in this method. This method CAN BE called to clean-up
        // after concurrent device adds/updates and thus counting here
        // is misleading
        ArrayList<Entity> emptyToKeep = new ArrayList<Entity>();
        for (Entity entity : device.getEntities()) {
            this.removeEntity(entity, device.getEntityClass(),
                device.getDeviceKey(), emptyToKeep);
        }
        if (!deviceMap.remove(device.getDeviceKey(), device)) {
            if (logger.isDebugEnabled())
                logger.debug("device map does not have this device -" +
                    device.toString());
        }
    }

    private EnumSet<DeviceField> getEntityKeys(Long macAddress,
                                               Short vlan,
                                               Integer ipv4Address,
                                               Long switchDPID,
                                               Integer switchPort) {
        // FIXME: vlan==null is a valid search. Need to handle this
        // case correctly. Note that the code will still work correctly.
        // But we might do a full device search instead of using an index.
        EnumSet<DeviceField> keys = EnumSet.noneOf(DeviceField.class);
        if (macAddress != null) keys.add(DeviceField.MAC);
        if (vlan != null) keys.add(DeviceField.VLAN);
        if (ipv4Address != null) keys.add(DeviceField.IPV4);
        if (switchDPID != null) keys.add(DeviceField.SWITCH);
        if (switchPort != null) keys.add(DeviceField.PORT);
        return keys;
    }

    protected Iterator<Device> queryClassByEntity(IEntityClass clazz,
                                                  EnumSet<DeviceField> keyFields,
                                                  Entity entity) {
        ClassState classState = getClassState(clazz);
        DeviceIndex index = classState.secondaryIndexMap.get(keyFields);
        if (index == null) return Collections.<Device>emptySet().iterator();
        return new DeviceIndexInterator(this, index.queryByEntity(entity));
    }

    protected Device allocateDevice(Long deviceKey,
                                    Entity entity,
                                    IEntityClass entityClass) {
        return new Device(this, deviceKey, entity, entityClass);
    }

    // TODO: FIX THIS.
    protected Device allocateDevice(Long deviceKey,
                                    String dhcpClientName,
                                    List<AttachmentPoint> aps,
                                    List<AttachmentPoint> trueAPs,
                                    Collection<Entity> entities,
                                    IEntityClass entityClass) {
        return new Device(this, deviceKey, dhcpClientName, aps, trueAPs,
                          entities, entityClass);
    }

    protected Device allocateDevice(Device device,
                                    Entity entity,
                                    int insertionpoint) {
        return new Device(device, entity, insertionpoint);
    }

    //not used
    protected Device allocateDevice(Device device, Set <Entity> entities) {
        List <AttachmentPoint> newPossibleAPs =
                new ArrayList<AttachmentPoint>();
        List <AttachmentPoint> newAPs =
                new ArrayList<AttachmentPoint>();
        for (Entity entity : entities) {
            if (entity.switchDPID != null && entity.switchPort != null) {
                AttachmentPoint aP =
                        new AttachmentPoint(entity.switchDPID.longValue(),
                                    entity.switchPort.shortValue(), 0);
                newPossibleAPs.add(aP);
            }
        }
        if (device.attachmentPoints != null) {
            for (AttachmentPoint oldAP : device.attachmentPoints) {
                if (newPossibleAPs.contains(oldAP)) {
                    newAPs.add(oldAP);
                }
            }
        }
        if (newAPs.isEmpty())
            newAPs = null;
        Device d = new Device(this, device.getDeviceKey(),
                              device.getDHCPClientName(), newAPs, null,
                              entities, device.getEntityClass());
        d.updateAttachmentPoint();
        return d;
    }

    // *********************
    // ITopologyListener
    // *********************

    /**
     * Topology listener method.
     */
    @Override
    public void topologyChanged(List<LDUpdate> updateList) {
        Iterator<Device> diter = deviceMap.values().iterator();
        if (updateList != null) {
            if (logger.isTraceEnabled()) {
                for(LDUpdate update: updateList) {
                    logger.trace("Topo update: {}", update);
                }
            }
        }

        while (diter.hasNext()) {
            Device d = diter.next();
            if (d.updateAttachmentPoint()) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Attachment point changed for device: {}", d);
                }
                sendDeviceMovedNotification(d);
            }
        }
        // Since topologyChanged() does not occur in the packet-in pipeline,
        // debugEvents need to be flushed explicitly
        debugEvents.flushEvents();
    }

    /**
     * Send update notifications to listeners
     * @param updates the updates to process.
     */
    protected void sendDeviceMovedNotification(Device d) {
        cntDeviceMoved.updateCounterNoFlush();
        deviceSyncManager.storeDevice(d);
        List<IDeviceListener> listeners = deviceListeners.getOrderedListeners();
        if (listeners != null) {
            for (IDeviceListener listener : listeners) {
                listener.deviceMoved(d);
            }
        }
    }

    // *********************
    // IEntityClassListener
    // *********************

    @Override
    public void entityClassChanged (Set<String> entityClassNames) {
        /* iterate through the devices, reclassify the devices that belong
         * to these entity class names
         */
        Iterator<Device> diter = deviceMap.values().iterator();
        while (diter.hasNext()) {
            Device d = diter.next();
            if (d.getEntityClass() == null ||
                entityClassNames.contains(d.getEntityClass().getName()))
                reclassifyDevice(d);
        }
    }

    /**
     * this method will reclassify and reconcile a device - possibilities
     * are - create new device(s), remove entities from this device. If the
     * device entity class did not change then it returns false else true.
     * @param device
     */
    protected boolean reclassifyDevice(Device device)
    {
        // first classify all entities of this device
        if (device == null) {
            logger.debug("In reclassify for null device");
            return false;
        }
        boolean needToReclassify = false;
        for (Entity entity : device.entities) {
            IEntityClass entityClass =
                    this.entityClassifier.classifyEntity(entity);
            if (entityClass == null || device.getEntityClass() == null) {
                needToReclassify = true;
                break;
            }
            if (!entityClass.getName().
                    equals(device.getEntityClass().getName())) {
                needToReclassify = true;
                break;
            }
        }
        if (needToReclassify == false) {
            return false;
        }

        cntDeviceReclassifyDelete.updateCounterNoFlush();
        LinkedList<DeviceUpdate> deviceUpdates =
                new LinkedList<DeviceUpdate>();
        // delete this device and then re-learn all the entities
        this.deleteDevice(device);
        deviceUpdates.add(new DeviceUpdate(device,
                DeviceUpdate.Change.DELETE, null));
        if (!deviceUpdates.isEmpty())
            processUpdates(deviceUpdates);
        for (Entity entity: device.entities ) {
            this.learnDeviceByEntity(entity);
        }
        // Since reclassifyDevices() is not called in the packet-in pipeline,
        // debugEvents need to be flushed explicitly
        debugEvents.flushEvents();
        return true;
    }

    /**
     * For testing: sets the interval between writes of the same device
     * to the device store.
     * @param intervalMs
     */
    void setSyncStoreWriteInterval(int intervalMs) {
        this.syncStoreWriteIntervalMs = intervalMs;
    }

    /**
     * For testing: sets the time between transition to MASTER and
     * consolidate store
     * @param intervalMs
     */
    void setInitialSyncStoreConsolidateMs(int intervalMs) {
        this.initialSyncStoreConsolidateMs = intervalMs;
    }

    /**
     * For testing: consolidate the store NOW
     */
    void scheduleConsolidateStoreNow() {
        this.storeConsolidateTask.reschedule(0, TimeUnit.MILLISECONDS);
    }

    private class DeviceSyncManager  {
        // maps (opaque) deviceKey to the time in System.nanoTime() when we
        // last wrote the device to the sync store
        private final ConcurrentMap<Long, Long> lastWriteTimes =
                new ConcurrentHashMap<Long, Long>();

        /**
         * Write the given device to storage if we are MASTER.
         * Use this method if the device has significantly changed (e.g.,
         * new AP, new IP, entities removed).
         * @param d the device to store
         */
        public void storeDevice(Device d) {
            if (!isMaster)
                return;
            if (d == null)
                return;
            long now = System.nanoTime();
            writeUpdatedDeviceToStorage(d);
            lastWriteTimes.put(d.getDeviceKey(), now);
        }

        /**
         * Write the given device to storage if we are MASTER and if the
         * last write for the device was more than this.syncStoreIntervalNs
         * time ago.
         * Use this method to updated last active times in the store.
         * @param d the device to store
         */
        public void storeDeviceThrottled(Device d) {
            long intervalNs = syncStoreWriteIntervalMs*1000L*1000L;
            if (!isMaster)
                return;
            if (d == null)
                return;
            long now = System.nanoTime();
            Long last = lastWriteTimes.get(d.getDeviceKey());
            if (last == null ||
                    now - last > intervalNs) {
                writeUpdatedDeviceToStorage(d);
                lastWriteTimes.put(d.getDeviceKey(), now);
            } else {
                cntDeviceStoreThrottled.updateCounterWithFlush();
            }
        }

        /**
         * Remove the given device from the store. If only some entities have
         * been removed the updated device should be written using
         * {@link #storeDevice(Device)}
         * @param d
         */
        public void removeDevice(Device d) {
            if (!isMaster)
                return;
            // FIXME: could we have a problem with concurrent put to the
            // hashMap? I.e., we write a stale entry to the map after the
            // delete and now are left with an entry we'll never clean up
            lastWriteTimes.remove(d.getDeviceKey());
            try {
                // TODO: should probably do versioned delete. OTOH, even
                // if we accidentally delete, we'll write it again after
                // the next entity ....
                cntDeviceRemovedFromStore.updateCounterWithFlush();
                storeClient.delete(DeviceSyncRepresentation.computeKey(d));
            } catch(ObsoleteVersionException e) {
                // FIXME
            } catch (SyncException e) {
                cntSyncException.updateCounterWithFlush();
                logger.error("Could not remove device " + d + " from store", e);
            }
        }

        /**
         * Remove the given Versioned device from the store. If the device
         * was locally modified ignore the delete request.
         * @param syncedDeviceKey
         */
        private void removeDevice(Versioned<DeviceSyncRepresentation> dev) {
            try {
                cntDeviceRemovedFromStore.updateCounterWithFlush();
                storeClient.delete(dev.getValue().getKey(),
                                   dev.getVersion());
            } catch(ObsoleteVersionException e) {
                // Key was locally modified by another thread.
                // Do not delete and ignore.
            } catch(SyncException e) {
                cntSyncException.updateCounterWithFlush();
                logger.error("Failed to remove device entry for " +
                            dev.toString() + " from store.", e);
            }
        }

        /**
         * Synchronously transition from SLAVE to MASTER. By iterating through
         * the store and learning all devices from the store
         */
        private void goToMaster() {
            if (logger.isDebugEnabled()) {
                logger.debug("Transitioning to MASTER role");
            }
            cntTransitionToMaster.updateCounterWithFlush();
            IClosableIterator<Map.Entry<String,Versioned<DeviceSyncRepresentation>>>
                    iter = null;
            try {
                iter = storeClient.entries();
            } catch (SyncException e) {
                cntSyncException.updateCounterWithFlush();
                logger.error("Failed to read devices from sync store", e);
                return;
            }
            try {
                while(iter.hasNext()) {
                    Versioned<DeviceSyncRepresentation> versionedDevice =
                            iter.next().getValue();
                    DeviceSyncRepresentation storedDevice =
                            versionedDevice.getValue();
                    if (storedDevice == null)
                        continue;
                    cntDevicesFromStore.updateCounterWithFlush();
                    for(SyncEntity se: storedDevice.getEntities()) {
                        learnDeviceByEntity(se.asEntity());
                    }
                }
            } finally {
                if (iter != null)
                    iter.close();
            }
            storeConsolidateTask.reschedule(initialSyncStoreConsolidateMs,
                                            TimeUnit.MILLISECONDS);
        }

        /**
         * Actually perform the write of the device to the store
         * FIXME: concurrent modification behavior
         * @param device The device to write
         */
        private void writeUpdatedDeviceToStorage(Device device) {
            try {
                cntDeviceStrored.updateCounterWithFlush();
                // FIXME: use a versioned put
                DeviceSyncRepresentation storeDevice =
                        new DeviceSyncRepresentation(device);
                storeClient.put(storeDevice.getKey(), storeDevice);
            } catch (ObsoleteVersionException e) {
                // FIXME: what's the right behavior here. Can the store client
                // even throw this error?
            } catch (SyncException e) {
                cntSyncException.updateCounterWithFlush();
                logger.error("Could not write device " + device +
                          " to sync store:", e);
            }
        }

        /**
         * Iterate through all entries in the sync store. For each device
         * in the store check if any stored entity matches a live device. If
         * no entities match a live device we remove the entry from the store.
         *
         * Note: we do not check if all devices known to device manager are
         * in the store. We rely on regular packetIns for that.
         * Note: it's possible that multiple entries in the store map to the
         * same device. We don't check or handle this case.
         *
         * We need to perform this check after a SLAVE->MASTER transition to
         * get rid of all entries the old master might have written to the
         * store after we took over. We also run it regularly in MASTER
         * state to ensure we don't have stale entries in the store
         */
        private void consolidateStore() {
            if (!isMaster)
                return;
            cntConsolidateStoreRuns.updateCounterWithFlush();
            if (logger.isDebugEnabled()) {
                logger.debug("Running consolidateStore.");
            }
            IClosableIterator<Map.Entry<String,Versioned<DeviceSyncRepresentation>>>
                    iter = null;
            try {
                iter = storeClient.entries();
            } catch (SyncException e) {
                cntSyncException.updateCounterWithFlush();
                logger.error("Failed to read devices from sync store", e);
                return;
            }
            try {
                while(iter.hasNext()) {
                    boolean found = false;
                    Versioned<DeviceSyncRepresentation> versionedDevice =
                            iter.next().getValue();
                    DeviceSyncRepresentation storedDevice =
                            versionedDevice.getValue();
                    if (storedDevice == null)
                        continue;
                    for(SyncEntity se: storedDevice.getEntities()) {
                        try {
                            // Do we have a device for this entity??
                            IDevice d = findDevice(se.macAddress, se.vlan,
                                                   se.ipv4Address,
                                                   se.switchDPID,
                                                   se.switchPort);
                            if (d != null) {
                                found = true;
                                break;
                            }
                        } catch (IllegalArgumentException e) {
                            // not all key fields provided. Skip entity
                        }
                    }
                    if (!found) {
                        // We currently DO NOT have a live device that
                        // matches the current device from the store.
                        // Delete device from store.
                        if (logger.isDebugEnabled()) {
                            logger.debug("Removing device {} from store. No "
                                         + "corresponding live device",
                                         storedDevice.getKey());
                        }
                        cntConsolidateStoreDevicesRemoved.updateCounterWithFlush();
                        removeDevice(versionedDevice);
                    }
                }
            } finally {
                if (iter != null)
                    iter.close();
            }
        }
    }


    /**
     * For testing. Sets the syncService. Only call after init but before
     * startUp. Used by MockDeviceManager
     * @param syncService
     */
    protected void setSyncServiceIfNotSet(ISyncService syncService) {
        if (this.syncService == null)
            this.syncService = syncService;
    }

    /**
     * For testing.
     * @return
     */
    IHAListener getHAListener() {
        return this.haListenerDelegate;
    }

    /**
     * Device Event Class used to log Device related events
     */
    private class DeviceEvent {
        @EventColumn(name = "MAC", description = EventFieldType.MAC)
        private final long macAddress;
        @EventColumn(name = "IPs", description = EventFieldType.LIST_IPV4)
        private final List<Integer> ipv4Addresses;
        @EventColumn(name = "Old Attachment Points",
                     description = EventFieldType.LIST_ATTACHMENT_POINT)
        private final List<SwitchPort> oldAttachmentPoints;
        @EventColumn(name = "Current Attachment Points",
                     description = EventFieldType.LIST_ATTACHMENT_POINT)
        private final List<SwitchPort> currentAttachmentPoints;
        @EventColumn(name = "VLAN IDs", description = EventFieldType.LIST_OBJECT)
        private final List<Short> vlanIds;
        @EventColumn(name = "Reason", description = EventFieldType.STRING)
        private final String reason;

        public DeviceEvent(long macAddress, List<Integer> ipv4Addresses,
                List<SwitchPort> oldAttachmentPoints,
                List<SwitchPort> currentAttachmentPoints,
                List<Short> vlanIds, String reason) {
            super();
            this.macAddress = macAddress;
            this.ipv4Addresses = ipv4Addresses;
            this.oldAttachmentPoints = oldAttachmentPoints;
            this.currentAttachmentPoints = currentAttachmentPoints;
            this.vlanIds = vlanIds;
            this.reason = reason;
        }
    }
}
TOP

Related Classes of net.floodlightcontroller.devicemanager.internal.DeviceManagerImpl$HAListenerDelegate

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.