lldp discovery independent of OF

Change-Id: I720f727f6628e30e5d732e6d7bf742d1b7050812
diff --git a/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java b/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java
new file mode 100644
index 0000000..fc0a0f4
--- /dev/null
+++ b/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java
@@ -0,0 +1,349 @@
+/*******************************************************************************
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ******************************************************************************/
+package org.onlab.onos.provider.lldp.impl;
+
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jboss.netty.util.Timeout;
+import org.jboss.netty.util.TimerTask;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.Device;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Link.Type;
+import org.onlab.onos.net.Port;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.flow.DefaultTrafficTreatment;
+import org.onlab.onos.net.link.DefaultLinkDescription;
+import org.onlab.onos.net.link.LinkDescription;
+import org.onlab.onos.net.link.LinkProviderService;
+import org.onlab.onos.net.packet.DefaultOutboundPacket;
+import org.onlab.onos.net.packet.OutboundPacket;
+import org.onlab.onos.net.packet.PacketContext;
+import org.onlab.onos.net.packet.PacketService;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.ONOSLLDP;
+import org.onlab.util.Timer;
+import org.slf4j.Logger;
+
+
+
+/**
+ * Run discovery process from a physical switch. Ports are initially labeled as
+ * slow ports. When an LLDP is successfully received, label the remote port as
+ * fast. Every probeRate milliseconds, loop over all fast ports and send an
+ * LLDP, send an LLDP for a single slow port. Based on FlowVisor topology
+ * discovery implementation.
+ *
+ * TODO: add 'fast discovery' mode: drop LLDPs in destination switch but listen
+ * for flow_removed messages
+ */
+public class LinkDiscovery implements TimerTask {
+
+    private final Device device;
+    // send 1 probe every probeRate milliseconds
+    private final long probeRate;
+    private final Set<Long> slowPorts;
+    private final Set<Long> fastPorts;
+    // number of unacknowledged probes per port
+    private final Map<Long, AtomicInteger> portProbeCount;
+    // number of probes to send before link is removed
+    private static final short MAX_PROBE_COUNT = 3;
+    private final Logger log = getLogger(getClass());
+    private final ONOSLLDP lldpPacket;
+    private final Ethernet ethPacket;
+    private Ethernet bddpEth;
+    private final boolean useBDDP;
+    private final LinkProviderService linkProvider;
+    private final PacketService pktService;
+    private Timeout timeout;
+
+    /**
+     * Instantiates discovery manager for the given physical switch. Creates a
+     * generic LLDP packet that will be customized for the port it is sent out on.
+     * Starts the the timer for the discovery process.
+     *
+     * @param device the physical switch
+     * @param useBDDP flag to also use BDDP for discovery
+     */
+    public LinkDiscovery(Device device, PacketService pktService,
+             LinkProviderService providerService, Boolean... useBDDP) {
+        this.device = device;
+        this.probeRate = 3000;
+        this.linkProvider = providerService;
+        this.pktService = pktService;
+        this.slowPorts = Collections.synchronizedSet(new HashSet<Long>());
+        this.fastPorts = Collections.synchronizedSet(new HashSet<Long>());
+        this.portProbeCount = new HashMap<>();
+        this.lldpPacket = new ONOSLLDP();
+        this.lldpPacket.setChassisId(device.chassisId());
+        this.lldpPacket.setDevice(device.id().toString());
+
+
+        this.ethPacket = new Ethernet();
+        this.ethPacket.setEtherType(Ethernet.TYPE_LLDP);
+        this.ethPacket.setDestinationMACAddress(ONOSLLDP.LLDP_NICIRA);
+        this.ethPacket.setPayload(this.lldpPacket);
+        this.ethPacket.setPad(true);
+        this.useBDDP = useBDDP.length > 0 ? useBDDP[0] : false;
+        if (this.useBDDP) {
+            this.bddpEth = new Ethernet();
+            this.bddpEth.setPayload(this.lldpPacket);
+            this.bddpEth.setEtherType(Ethernet.TYPE_BSN);
+            this.bddpEth.setDestinationMACAddress(ONOSLLDP.BDDP_MULTICAST);
+            this.bddpEth.setPad(true);
+            log.info("Using BDDP to discover network");
+        }
+
+        start();
+        this.log.debug("Started discovery manager for switch {}",
+                       device.id());
+
+    }
+
+    /**
+     * Add physical port port to discovery process.
+     * Send out initial LLDP and label it as slow port.
+     *
+     * @param port the port
+     */
+    public void addPort(final Port port) {
+        this.log.debug("sending init probe to port {}",
+                           port.number().toLong());
+
+        sendProbes(port.number().toLong());
+
+        synchronized (this) {
+            this.slowPorts.add(port.number().toLong());
+        }
+
+
+    }
+
+    /**
+     * Removes physical port from discovery process.
+     *
+     * @param port the port
+     */
+    public void removePort(final Port port) {
+        // Ignore ports that are not on this switch
+
+        long portnum = port.number().toLong();
+        synchronized (this) {
+            if (this.slowPorts.contains(portnum)) {
+                this.slowPorts.remove(portnum);
+
+            } else if (this.fastPorts.contains(portnum)) {
+                this.fastPorts.remove(portnum);
+                this.portProbeCount.remove(portnum);
+                // no iterator to update
+            } else {
+                this.log.warn(
+                        "tried to dynamically remove non-existing port {}",
+                        portnum);
+            }
+        }
+    }
+
+    /**
+     * Method called by remote port to acknowledge receipt of LLDP sent by
+     * this port. If slow port, updates label to fast. If fast port, decrements
+     * number of unacknowledged probes.
+     *
+     * @param portNumber the port
+     */
+    public void ackProbe(final Long portNumber) {
+
+        synchronized (this) {
+            if (this.slowPorts.contains(portNumber)) {
+                this.log.debug("Setting slow port to fast: {}:{}",
+                        this.device.id(), portNumber);
+                this.slowPorts.remove(portNumber);
+                this.fastPorts.add(portNumber);
+                this.portProbeCount.put(portNumber, new AtomicInteger(0));
+            } else {
+                if (this.fastPorts.contains(portNumber)) {
+                    this.portProbeCount.get(portNumber).set(0);
+                } else {
+                    this.log.debug(
+                            "Got ackProbe for non-existing port: {}",
+                            portNumber);
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Handles an incoming LLDP packet. Creates link in topology and sends ACK
+     * to port where LLDP originated.
+     */
+    public boolean handleLLDP(PacketContext context) {
+        Ethernet eth = context.inPacket().parsed();
+        ONOSLLDP onoslldp = ONOSLLDP.parseONOSLLDP(eth);
+        if (onoslldp != null) {
+            final PortNumber dstPort =
+                    context.inPacket().receivedFrom().port();
+            final PortNumber srcPort = PortNumber.portNumber(onoslldp.getPort());
+            final DeviceId srcDeviceId = DeviceId.deviceId(onoslldp.getDeviceString());
+            final DeviceId dstDeviceId = context.inPacket().receivedFrom().deviceId();
+            this.ackProbe(srcPort.toLong());
+            ConnectPoint src = new ConnectPoint(srcDeviceId, srcPort);
+            ConnectPoint dst = new ConnectPoint(dstDeviceId, dstPort);
+
+            LinkDescription ld;
+            if (eth.getEtherType() == Ethernet.TYPE_BSN) {
+                ld = new DefaultLinkDescription(src, dst, Type.INDIRECT);
+            } else {
+                ld = new DefaultLinkDescription(src, dst, Type.DIRECT);
+            }
+            linkProvider.linkDetected(ld);
+            return true;
+        }
+        return false;
+    }
+
+
+
+    /**
+     * Execute this method every t milliseconds. Loops over all ports
+     * labeled as fast and sends out an LLDP. Send out an LLDP on a single slow
+     * port.
+     *
+     * @param t timeout
+     * @throws Exception
+     */
+    @Override
+    public void run(final Timeout t) {
+        this.log.debug("sending probes");
+        synchronized (this) {
+            final Iterator<Long> fastIterator = this.fastPorts.iterator();
+            Long portNumber;
+            Integer probeCount;
+            while (fastIterator.hasNext()) {
+                portNumber = fastIterator.next();
+                probeCount = this.portProbeCount.get(portNumber)
+                        .getAndIncrement();
+
+                if (probeCount < LinkDiscovery.MAX_PROBE_COUNT) {
+                    this.log.debug("sending fast probe to port");
+                    sendProbes(portNumber);
+                } else {
+                    // Update fast and slow ports
+                    //fastIterator.remove();
+                    //this.slowPorts.add(portNumber);
+                    //this.portProbeCount.remove(portNumber);
+
+
+                    ConnectPoint cp = new ConnectPoint(
+                            device.id(),
+                            PortNumber.portNumber(portNumber));
+                    log.debug("Link down -> {}", cp);
+                    linkProvider.linksVanished(cp);
+                }
+            }
+
+            // send a probe for the next slow port
+            if (!this.slowPorts.isEmpty()) {
+                Iterator<Long> slowIterator = this.slowPorts.iterator();
+                while (slowIterator.hasNext()) {
+                    portNumber = slowIterator.next();
+                    this.log.debug("sending slow probe to port {}", portNumber);
+
+                    sendProbes(portNumber);
+
+                }
+            }
+        }
+
+        // reschedule timer
+        timeout = Timer.getTimer().newTimeout(this, this.probeRate,
+                TimeUnit.MILLISECONDS);
+    }
+
+    public void stop() {
+        timeout.cancel();
+    }
+
+    public void start() {
+        timeout = Timer.getTimer().newTimeout(this, 0,
+                                              TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Creates packet_out LLDP for specified output port.
+     *
+     * @param port the port
+     * @return Packet_out message with LLDP data
+     */
+    private OutboundPacket createOutBoundLLDP(final Long port) {
+        if (port == null) {
+            return null;
+        }
+        this.lldpPacket.setPortId(port.intValue());
+        this.ethPacket.setSourceMACAddress("DE:AD:BE:EF:BA:11");
+
+        final byte[] lldp = this.ethPacket.serialize();
+        OutboundPacket outboundPacket = new DefaultOutboundPacket(
+                this.device.id(),
+                DefaultTrafficTreatment.builder().setOutput(
+                        PortNumber.portNumber(port)).build(),
+                ByteBuffer.wrap(lldp));
+        return outboundPacket;
+    }
+
+    /**
+     * Creates packet_out BDDP for specified output port.
+     *
+     * @param port the port
+     * @return Packet_out message with LLDP data
+     */
+    private OutboundPacket createOutBoundBDDP(final Long port) {
+        if (port == null) {
+            return null;
+        }
+        this.lldpPacket.setPortId(port.intValue());
+        this.bddpEth.setSourceMACAddress("DE:AD:BE:EF:BA:11");
+
+        final byte[] bddp = this.bddpEth.serialize();
+        OutboundPacket outboundPacket = new DefaultOutboundPacket(
+                this.device.id(),
+                DefaultTrafficTreatment.builder()
+                        .setOutput(PortNumber.portNumber(port)).build(),
+                ByteBuffer.wrap(bddp));
+        return outboundPacket;
+    }
+
+    private void sendProbes(Long portNumber) {
+        OutboundPacket pkt = this.createOutBoundLLDP(portNumber);
+        pktService.emit(pkt);
+        if (useBDDP) {
+            OutboundPacket bpkt = this.createOutBoundBDDP(portNumber);
+            pktService.emit(bpkt);
+        }
+    }
+
+}