Merge branch 'master' of ssh://gerrit.onlab.us:29418/onos-next

Conflicts:
	core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoPoolUtil.java

Change-Id: I6a8fc884f0c0b6578762d7b439177378e838735f
diff --git a/apps/fwd/pom.xml b/apps/fwd/pom.xml
index 4ee2dc3..b203121 100644
--- a/apps/fwd/pom.xml
+++ b/apps/fwd/pom.xml
@@ -16,4 +16,11 @@
 
     <description>ONOS simple reactive forwarding app</description>
 
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+    </dependencies>
+
 </project>
diff --git a/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java b/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java
index 8ead67f..62b0b84 100644
--- a/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java
+++ b/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java
@@ -1,12 +1,10 @@
 package org.onlab.onos.fwd;
 
-import static org.slf4j.LoggerFactory.getLogger;
-
-import java.util.Set;
-
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
+import org.apache.felix.scr.annotations.Property;
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.onlab.onos.ApplicationId;
@@ -29,8 +27,14 @@
 import org.onlab.onos.net.packet.PacketService;
 import org.onlab.onos.net.topology.TopologyService;
 import org.onlab.packet.Ethernet;
+import org.osgi.service.component.ComponentContext;
 import org.slf4j.Logger;
 
+import java.util.Dictionary;
+import java.util.Set;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
 /**
  * Sample reactive forwarding application.
  */
@@ -61,6 +65,10 @@
 
     private ApplicationId appId;
 
+    @Property(name = "enabled", boolValue = true,
+              label = "Enable forwarding; default is true")
+    private boolean isEnabled = true;
+
     @Activate
     public void activate() {
         appId = coreService.registerApplication("org.onlab.onos.fwd");
@@ -76,6 +84,22 @@
         log.info("Stopped");
     }
 
+    @Modified
+    public void modified(ComponentContext context) {
+        Dictionary properties = context.getProperties();
+        String flag = (String) properties.get("enabled");
+        if (flag != null) {
+            boolean enabled = flag.equals("true");
+            if (isEnabled != enabled) {
+                isEnabled = enabled;
+                if (!isEnabled) {
+                    flowRuleService.removeFlowRulesById(appId);
+                }
+                log.info("Reconfigured. Forwarding is {}",
+                         isEnabled ? "enabled" : "disabled");
+            }
+        }
+    }
 
     /**
      * Packet processor responsible for forwarding packets along their paths.
@@ -86,7 +110,7 @@
         public void process(PacketContext context) {
             // Stop processing if the packet has been handled, since we
             // can't do any more to it.
-            if (context.isHandled()) {
+            if (!isEnabled || context.isHandled()) {
                 return;
             }
 
@@ -114,8 +138,8 @@
             // Otherwise, get a set of paths that lead from here to the
             // destination edge switch.
             Set<Path> paths = topologyService.getPaths(topologyService.currentTopology(),
-                    pkt.receivedFrom().deviceId(),
-                    dst.location().deviceId());
+                                                       pkt.receivedFrom().deviceId(),
+                                                       dst.location().deviceId());
             if (paths.isEmpty()) {
                 // If there are no paths, flood and bail.
                 flood(context);
@@ -127,8 +151,8 @@
             Path path = pickForwardPath(paths, pkt.receivedFrom().port());
             if (path == null) {
                 log.warn("Doh... don't know where to go... {} -> {} received on {}",
-                        ethPkt.getSourceMAC(), ethPkt.getDestinationMAC(),
-                        pkt.receivedFrom());
+                         ethPkt.getSourceMAC(), ethPkt.getDestinationMAC(),
+                         pkt.receivedFrom());
                 flood(context);
                 return;
             }
@@ -152,7 +176,7 @@
     // Floods the specified packet if permissible.
     private void flood(PacketContext context) {
         if (topologyService.isBroadcastPoint(topologyService.currentTopology(),
-                context.inPacket().receivedFrom())) {
+                                             context.inPacket().receivedFrom())) {
             packetOut(context, PortNumber.FLOOD);
         } else {
             context.block();
@@ -174,18 +198,17 @@
         Ethernet inPkt = context.inPacket().parsed();
         TrafficSelector.Builder builder = DefaultTrafficSelector.builder();
         builder.matchEthType(inPkt.getEtherType())
-        .matchEthSrc(inPkt.getSourceMAC())
-        .matchEthDst(inPkt.getDestinationMAC())
-        .matchInport(context.inPacket().receivedFrom().port());
+                .matchEthSrc(inPkt.getSourceMAC())
+                .matchEthDst(inPkt.getDestinationMAC())
+                .matchInport(context.inPacket().receivedFrom().port());
 
         TrafficTreatment.Builder treat = DefaultTrafficTreatment.builder();
         treat.setOutput(portNumber);
 
         FlowRule f = new DefaultFlowRule(context.inPacket().receivedFrom().deviceId(),
-                builder.build(), treat.build(), PRIORITY, appId, TIMEOUT);
+                                         builder.build(), treat.build(), PRIORITY, appId, TIMEOUT);
 
         flowRuleService.applyFlowRules(f);
-
     }
 
 }
diff --git a/apps/sdnip/pom.xml b/apps/sdnip/pom.xml
index c8db20d..390b429 100644
--- a/apps/sdnip/pom.xml
+++ b/apps/sdnip/pom.xml
@@ -36,6 +36,12 @@
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
     </dependency>
+
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onlab-thirdparty</artifactId>
+    </dependency>
+
   </dependencies>
 
 </project>
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivity.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivity.java
index e17206d..31cd971 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivity.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivity.java
@@ -126,8 +126,8 @@
                  TrafficSelector selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toRealInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toRealInt(), IPV4_BIT_LENGTH))
+                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
+                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
                         .matchTcpDst(BGP_PORT)
                         .build();
 
@@ -147,8 +147,8 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toRealInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toRealInt(), IPV4_BIT_LENGTH))
+                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
+                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
                         .matchTcpSrc(BGP_PORT)
                         .build();
 
@@ -165,8 +165,8 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toRealInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toRealInt(), IPV4_BIT_LENGTH))
+                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
+                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
                         .matchTcpDst(BGP_PORT)
                         .build();
 
@@ -183,8 +183,8 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toRealInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toRealInt(), IPV4_BIT_LENGTH))
+                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
+                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
                         .matchTcpSrc(BGP_PORT)
                         .build();
 
@@ -251,8 +251,8 @@
                 TrafficSelector selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_ICMP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toRealInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toRealInt(), IPV4_BIT_LENGTH))
+                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
+                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
                         .build();
 
                 TrafficTreatment treatment = DefaultTrafficTreatment.builder()
@@ -269,8 +269,8 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_ICMP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toRealInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toRealInt(), IPV4_BIT_LENGTH))
+                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
+                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
                         .build();
 
                 PointToPointIntent reversedIntent = new PointToPointIntent(
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteEntry.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteEntry.java
new file mode 100644
index 0000000..f8d659c
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteEntry.java
@@ -0,0 +1,100 @@
+package org.onlab.onos.sdnip;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Represents a route entry for an IP prefix.
+ */
+public class RouteEntry {
+    private final IpPrefix prefix;             // The IP prefix
+    private final IpAddress nextHop;           // Next-hop IP address
+
+    /**
+     * Class constructor.
+     *
+     * @param prefix the IP prefix of the route
+     * @param nextHop the next hop IP address for the route
+     */
+    public RouteEntry(IpPrefix prefix, IpAddress nextHop) {
+        this.prefix = checkNotNull(prefix);
+        this.nextHop = checkNotNull(nextHop);
+    }
+
+    /**
+     * Returns the IP prefix of the route.
+     *
+     * @return the IP prefix of the route
+     */
+    public IpPrefix prefix() {
+        return prefix;
+    }
+
+    /**
+     * Returns the next hop IP address for the route.
+     *
+     * @return the next hop IP address for the route
+     */
+    public IpAddress nextHop() {
+        return nextHop;
+    }
+
+    /**
+     * Creates the binary string representation of an IPv4 prefix.
+     * The string length is equal to the prefix length.
+     *
+     * @param ip4Prefix the IPv4 prefix to use
+     * @return the binary string representation
+     */
+    static String createBinaryString(IpPrefix ip4Prefix) {
+        if (ip4Prefix.prefixLength() == 0) {
+            return "";
+        }
+
+        StringBuilder result = new StringBuilder(ip4Prefix.prefixLength());
+        long value = ip4Prefix.toInt();
+        for (int i = 0; i < ip4Prefix.prefixLength(); i++) {
+            long mask = 1 << (IpAddress.MAX_INET_MASK - 1 - i);
+            result.append(((value & mask) == 0) ? "0" : "1");
+        }
+        return result.toString();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        //
+        // NOTE: Subclasses are considered as change of identity, hence
+        // equals() will return false if the class type doesn't match.
+        //
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        RouteEntry otherRoute = (RouteEntry) other;
+        return Objects.equals(this.prefix, otherRoute.prefix) &&
+            Objects.equals(this.nextHop, otherRoute.nextHop);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(prefix, nextHop);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+            .add("prefix", prefix)
+            .add("nextHop", nextHop)
+            .toString();
+    }
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteListener.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteListener.java
new file mode 100644
index 0000000..424e348
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteListener.java
@@ -0,0 +1,13 @@
+package org.onlab.onos.sdnip;
+
+/**
+ * An interface to receive route updates from route providers.
+ */
+public interface RouteListener {
+    /**
+     * Receives a route update from a route provider.
+     *
+     * @param routeUpdate the updated route information
+     */
+    public void update(RouteUpdate routeUpdate);
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteUpdate.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteUpdate.java
new file mode 100644
index 0000000..a134a7a
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/RouteUpdate.java
@@ -0,0 +1,91 @@
+package org.onlab.onos.sdnip;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Represents a change in routing information.
+ */
+public class RouteUpdate {
+    private final Type type;                    // The route update type
+    private final RouteEntry routeEntry;        // The updated route entry
+
+    /**
+     * Specifies the type of a route update.
+     * <p/>
+     * Route updates can either provide updated information for a route, or
+     * withdraw a previously updated route.
+     */
+    public enum Type {
+        /**
+         * The update contains updated route information for a route.
+         */
+        UPDATE,
+        /**
+         * The update withdraws the route, meaning any previous information is
+         * no longer valid.
+         */
+        DELETE
+    }
+
+    /**
+     * Class constructor.
+     *
+     * @param type the type of the route update
+     * @param routeEntry the route entry with the update
+     */
+    public RouteUpdate(Type type, RouteEntry routeEntry) {
+        this.type = type;
+        this.routeEntry = checkNotNull(routeEntry);
+    }
+
+    /**
+     * Returns the type of the route update.
+     *
+     * @return the type of the update
+     */
+    public Type type() {
+        return type;
+    }
+
+    /**
+     * Returns the route entry the route update is for.
+     *
+     * @return the route entry the route update is for
+     */
+    public RouteEntry routeEntry() {
+        return routeEntry;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) {
+            return true;
+        }
+
+        if (!(other instanceof RouteUpdate)) {
+            return false;
+        }
+
+        RouteUpdate otherUpdate = (RouteUpdate) other;
+
+        return Objects.equals(this.type, otherUpdate.type) &&
+            Objects.equals(this.routeEntry, otherUpdate.routeEntry);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(type, routeEntry);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+            .add("type", type)
+            .add("routeEntry", routeEntry)
+            .toString();
+    }
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
new file mode 100644
index 0000000..2a1a655
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
@@ -0,0 +1,769 @@
+package org.onlab.onos.sdnip;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.Semaphore;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.Host;
+import org.onlab.onos.net.flow.DefaultTrafficSelector;
+import org.onlab.onos.net.flow.DefaultTrafficTreatment;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.flow.criteria.Criteria.IPCriterion;
+import org.onlab.onos.net.flow.criteria.Criterion;
+import org.onlab.onos.net.flow.criteria.Criterion.Type;
+import org.onlab.onos.net.host.HostEvent;
+import org.onlab.onos.net.host.HostListener;
+import org.onlab.onos.net.host.HostService;
+import org.onlab.onos.net.intent.Intent;
+import org.onlab.onos.net.intent.IntentId;
+import org.onlab.onos.net.intent.IntentService;
+import org.onlab.onos.net.intent.MultiPointToSinglePointIntent;
+import org.onlab.onos.sdnip.config.BgpPeer;
+import org.onlab.onos.sdnip.config.Interface;
+import org.onlab.onos.sdnip.config.SdnIpConfigService;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.SetMultimap;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.googlecode.concurrenttrees.common.KeyValuePair;
+import com.googlecode.concurrenttrees.radix.node.concrete.DefaultByteArrayNodeFactory;
+import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree;
+import com.googlecode.concurrenttrees.radixinverted.InvertedRadixTree;
+
+/**
+ * This class processes BGP route update, translates each update into a intent
+ * and submits the intent.
+ *
+ * TODO: Make it thread-safe.
+ */
+public class Router implements RouteListener {
+
+    private static final Logger log = LoggerFactory.getLogger(Router.class);
+
+    // Store all route updates in a InvertedRadixTree.
+    // The key in this Tree is the binary sting of prefix of route.
+    // The Ip4Address is the next hop address of route, and is also the value
+    // of each entry.
+    private InvertedRadixTree<RouteEntry> bgpRoutes;
+
+    // Stores all incoming route updates in a queue.
+    private BlockingQueue<RouteUpdate> routeUpdates;
+
+    // The Ip4Address is the next hop address of each route update.
+    private SetMultimap<IpAddress, RouteEntry> routesWaitingOnArp;
+    private ConcurrentHashMap<IpPrefix, MultiPointToSinglePointIntent> pushedRouteIntents;
+
+    private IntentService intentService;
+    //private IProxyArpService proxyArp;
+    private HostService hostService;
+    private SdnIpConfigService configInfoService;
+    private InterfaceService interfaceService;
+
+    private ExecutorService bgpUpdatesExecutor;
+    private ExecutorService bgpIntentsSynchronizerExecutor;
+
+    // TODO temporary
+    private int intentId = Integer.MAX_VALUE / 2;
+
+    //
+    // State to deal with SDN-IP Leader election and pushing Intents
+    //
+    private Semaphore intentsSynchronizerSemaphore = new Semaphore(0);
+    private volatile boolean isElectedLeader = false;
+    private volatile boolean isActivatedLeader = false;
+
+    // For routes announced by local BGP daemon in SDN network,
+    // the next hop will be 0.0.0.0.
+    public static final IpAddress LOCAL_NEXT_HOP = IpAddress.valueOf("0.0.0.0");
+
+    /**
+     * Class constructor.
+     *
+     * @param intentService the intent service
+     * @param proxyArp the proxy ARP service
+     * @param configInfoService the configuration service
+     * @param interfaceService the interface service
+     */
+    public Router(IntentService intentService, HostService hostService,
+            SdnIpConfigService configInfoService, InterfaceService interfaceService) {
+
+        this.intentService = intentService;
+        this.hostService = hostService;
+        this.configInfoService = configInfoService;
+        this.interfaceService = interfaceService;
+
+        bgpRoutes = new ConcurrentInvertedRadixTree<>(
+                new DefaultByteArrayNodeFactory());
+        routeUpdates = new LinkedBlockingQueue<>();
+        routesWaitingOnArp = Multimaps.synchronizedSetMultimap(
+                HashMultimap.<IpAddress, RouteEntry>create());
+        pushedRouteIntents = new ConcurrentHashMap<>();
+
+        bgpUpdatesExecutor = Executors.newSingleThreadExecutor(
+                new ThreadFactoryBuilder().setNameFormat("bgp-updates-%d").build());
+        bgpIntentsSynchronizerExecutor = Executors.newSingleThreadExecutor(
+                new ThreadFactoryBuilder()
+                .setNameFormat("bgp-intents-synchronizer-%d").build());
+
+        this.hostService.addListener(new InternalHostListener());
+    }
+
+    /**
+     * Starts the Router.
+     */
+    public void start() {
+
+        bgpUpdatesExecutor.execute(new Runnable() {
+            @Override
+            public void run() {
+                doUpdatesThread();
+            }
+        });
+
+        bgpIntentsSynchronizerExecutor.execute(new Runnable() {
+            @Override
+            public void run() {
+                doIntentSynchronizationThread();
+            }
+        });
+    }
+
+    //@Override TODO hook this up to something
+    public void leaderChanged(boolean isLeader) {
+        log.debug("Leader changed: {}", isLeader);
+
+        if (!isLeader) {
+            this.isElectedLeader = false;
+            this.isActivatedLeader = false;
+            return;                     // Nothing to do
+        }
+        this.isActivatedLeader = false;
+        this.isElectedLeader = true;
+
+        //
+        // Tell the Intents Synchronizer thread to start the synchronization
+        //
+        intentsSynchronizerSemaphore.release();
+    }
+
+    @Override
+    public void update(RouteUpdate routeUpdate) {
+        log.debug("Received new route Update: {}", routeUpdate);
+
+        try {
+            routeUpdates.put(routeUpdate);
+        } catch (InterruptedException e) {
+            log.debug("Interrupted while putting on routeUpdates queue", e);
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    /**
+     * Thread for Intent Synchronization.
+     */
+    private void doIntentSynchronizationThread() {
+        boolean interrupted = false;
+        try {
+            while (!interrupted) {
+                try {
+                    intentsSynchronizerSemaphore.acquire();
+                    //
+                    // Drain all permits, because a single synchronization is
+                    // sufficient.
+                    //
+                    intentsSynchronizerSemaphore.drainPermits();
+                } catch (InterruptedException e) {
+                    log.debug("Interrupted while waiting to become " +
+                              "Intent Synchronization leader");
+                    interrupted = true;
+                    break;
+                }
+                syncIntents();
+            }
+        } finally {
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * Thread for handling route updates.
+     */
+    private void doUpdatesThread() {
+        boolean interrupted = false;
+        try {
+            while (!interrupted) {
+                try {
+                    RouteUpdate update = routeUpdates.take();
+                    switch (update.type()) {
+                    case UPDATE:
+                        processRouteAdd(update.routeEntry());
+                        break;
+                    case DELETE:
+                        processRouteDelete(update.routeEntry());
+                        break;
+                    default:
+                        log.error("Unknown update Type: {}", update.type());
+                        break;
+                    }
+                } catch (InterruptedException e) {
+                    log.debug("Interrupted while taking from updates queue", e);
+                    interrupted = true;
+                } catch (Exception e) {
+                    log.debug("exception", e);
+                }
+            }
+        } finally {
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * Performs Intents Synchronization between the internally stored Route
+     * Intents and the installed Route Intents.
+     */
+    private void syncIntents() {
+        synchronized (this) {
+            if (!isElectedLeader) {
+                return;         // Nothing to do: not the leader anymore
+            }
+            log.debug("Syncing SDN-IP Route Intents...");
+
+            Map<IpPrefix, MultiPointToSinglePointIntent> fetchedIntents =
+                new HashMap<>();
+
+            //
+            // Fetch all intents, and classify the Multi-Point-to-Point Intents
+            // based on the matching prefix.
+            //
+            for (Intent intent : intentService.getIntents()) {
+                //
+                // TODO: Ignore all intents that are not installed by
+                // the SDN-IP application.
+                //
+                if (!(intent instanceof MultiPointToSinglePointIntent)) {
+                    continue;
+                }
+                MultiPointToSinglePointIntent mp2pIntent =
+                    (MultiPointToSinglePointIntent) intent;
+                /*Match match = mp2pIntent.getMatch();
+                if (!(match instanceof PacketMatch)) {
+                    continue;
+                }
+                PacketMatch packetMatch = (PacketMatch) match;
+                Ip4Prefix prefix = packetMatch.getDstIpAddress();
+                if (prefix == null) {
+                    continue;
+                }
+                fetchedIntents.put(prefix, mp2pIntent);*/
+                for (Criterion criterion : mp2pIntent.selector().criteria()) {
+                    if (criterion.type() == Type.IPV4_DST) {
+                        IPCriterion ipCriterion = (IPCriterion) criterion;
+                        fetchedIntents.put(ipCriterion.ip(), mp2pIntent);
+                    }
+                }
+
+            }
+
+            //
+            // Compare for each prefix the local IN-MEMORY Intents with the
+            // FETCHED Intents:
+            //  - If the IN-MEMORY Intent is same as the FETCHED Intent, store
+            //    the FETCHED Intent in the local memory (i.e., override the
+            //    IN-MEMORY Intent) to preserve the original Intent ID
+            //  - if the IN-MEMORY Intent is not same as the FETCHED Intent,
+            //    delete the FETCHED Intent, and push/install the IN-MEMORY
+            //    Intent.
+            //  - If there is an IN-MEMORY Intent for a prefix, but no FETCHED
+            //    Intent for same prefix, then push/install the IN-MEMORY
+            //    Intent.
+            //  - If there is a FETCHED Intent for a prefix, but no IN-MEMORY
+            //    Intent for same prefix, then delete/withdraw the FETCHED
+            //    Intent.
+            //
+            Collection<Pair<IpPrefix, MultiPointToSinglePointIntent>>
+                storeInMemoryIntents = new LinkedList<>();
+            Collection<Pair<IpPrefix, MultiPointToSinglePointIntent>>
+                addIntents = new LinkedList<>();
+            Collection<Pair<IpPrefix, MultiPointToSinglePointIntent>>
+                deleteIntents = new LinkedList<>();
+            for (Map.Entry<IpPrefix, MultiPointToSinglePointIntent> entry :
+                     pushedRouteIntents.entrySet()) {
+                IpPrefix prefix = entry.getKey();
+                MultiPointToSinglePointIntent inMemoryIntent =
+                    entry.getValue();
+                MultiPointToSinglePointIntent fetchedIntent =
+                    fetchedIntents.get(prefix);
+
+                if (fetchedIntent == null) {
+                    //
+                    // No FETCHED Intent for same prefix: push the IN-MEMORY
+                    // Intent.
+                    //
+                    addIntents.add(Pair.of(prefix, inMemoryIntent));
+                    continue;
+                }
+
+                //
+                // If IN-MEMORY Intent is same as the FETCHED Intent,
+                // store the FETCHED Intent in the local memory.
+                //
+                if (compareMultiPointToSinglePointIntents(inMemoryIntent,
+                                                          fetchedIntent)) {
+                    storeInMemoryIntents.add(Pair.of(prefix, fetchedIntent));
+                } else {
+                    //
+                    // The IN-MEMORY Intent is not same as the FETCHED Intent,
+                    // hence delete the FETCHED Intent, and install the
+                    // IN-MEMORY Intent.
+                    //
+                    deleteIntents.add(Pair.of(prefix, fetchedIntent));
+                    addIntents.add(Pair.of(prefix, inMemoryIntent));
+                }
+                fetchedIntents.remove(prefix);
+            }
+
+            //
+            // Any remaining FETCHED Intents have to be deleted/withdrawn
+            //
+            for (Map.Entry<IpPrefix, MultiPointToSinglePointIntent> entry :
+                     fetchedIntents.entrySet()) {
+                IpPrefix prefix = entry.getKey();
+                MultiPointToSinglePointIntent fetchedIntent = entry.getValue();
+                deleteIntents.add(Pair.of(prefix, fetchedIntent));
+            }
+
+            //
+            // Perform the actions:
+            // 1. Store in memory fetched intents that are same. Can be done
+            //    even if we are not the leader anymore
+            // 2. Delete intents: check if the leader before each operation
+            // 3. Add intents: check if the leader before each operation
+            //
+            for (Pair<IpPrefix, MultiPointToSinglePointIntent> pair :
+                     storeInMemoryIntents) {
+                IpPrefix prefix = pair.getLeft();
+                MultiPointToSinglePointIntent intent = pair.getRight();
+                log.debug("Intent synchronization: updating in-memory " +
+                          "Intent for prefix: {}", prefix);
+                pushedRouteIntents.put(prefix, intent);
+            }
+            //
+            isActivatedLeader = true;           // Allow push of Intents
+            for (Pair<IpPrefix, MultiPointToSinglePointIntent> pair :
+                     deleteIntents) {
+                IpPrefix prefix = pair.getLeft();
+                MultiPointToSinglePointIntent intent = pair.getRight();
+                if (!isElectedLeader) {
+                    isActivatedLeader = false;
+                    return;
+                }
+                log.debug("Intent synchronization: deleting Intent for " +
+                          "prefix: {}", prefix);
+                intentService.withdraw(intent);
+            }
+            //
+            for (Pair<IpPrefix, MultiPointToSinglePointIntent> pair :
+                     addIntents) {
+                IpPrefix prefix = pair.getLeft();
+                MultiPointToSinglePointIntent intent = pair.getRight();
+                if (!isElectedLeader) {
+                    isActivatedLeader = false;
+                    return;
+                }
+                log.debug("Intent synchronization: adding Intent for " +
+                          "prefix: {}", prefix);
+                intentService.submit(intent);
+            }
+            if (!isElectedLeader) {
+                isActivatedLeader = false;
+            }
+            log.debug("Syncing SDN-IP routes completed.");
+        }
+    }
+
+    /**
+     * Compares two Multi-point to Single Point Intents whether they represent
+     * same logical intention.
+     *
+     * @param intent1 the first Intent to compare
+     * @param intent2 the second Intent to compare
+     * @return true if both Intents represent same logical intention, otherwise
+     * false
+     */
+    private boolean compareMultiPointToSinglePointIntents(
+                                MultiPointToSinglePointIntent intent1,
+                                MultiPointToSinglePointIntent intent2) {
+        /*Match match1 = intent1.getMatch();
+        Match match2 = intent2.getMatch();
+        Action action1 = intent1.getAction();
+        Action action2 = intent2.getAction();
+        Set<SwitchPort> ingressPorts1 = intent1.getIngressPorts();
+        Set<SwitchPort> ingressPorts2 = intent2.getIngressPorts();
+        SwitchPort egressPort1 = intent1.getEgressPort();
+        SwitchPort egressPort2 = intent2.getEgressPort();
+
+        return Objects.equal(match1, match2) &&
+            Objects.equal(action1, action2) &&
+            Objects.equal(egressPort1, egressPort2) &&
+            Objects.equal(ingressPorts1, ingressPorts2);*/
+        return Objects.equal(intent1.selector(), intent2.selector()) &&
+                Objects.equal(intent1.treatment(), intent2.treatment()) &&
+                Objects.equal(intent1.ingressPoints(), intent2.ingressPoints()) &&
+                Objects.equal(intent1.egressPoint(), intent2.egressPoint());
+    }
+
+    /**
+     * Processes adding a route entry.
+     * <p/>
+     * Put new route entry into InvertedRadixTree. If there was an existing
+     * nexthop for this prefix, but the next hop was different, then execute
+     * deleting old route entry. If the next hop is the SDN domain, we do not
+     * handle it at the moment. Otherwise, execute adding a route.
+     *
+     * @param routeEntry the route entry to add
+     */
+    protected void processRouteAdd(RouteEntry routeEntry) {
+        synchronized (this) {
+            log.debug("Processing route add: {}", routeEntry);
+
+            IpPrefix prefix = routeEntry.prefix();
+            IpAddress nextHop = null;
+            RouteEntry foundRouteEntry =
+                bgpRoutes.put(RouteEntry.createBinaryString(prefix),
+                              routeEntry);
+            if (foundRouteEntry != null) {
+                nextHop = foundRouteEntry.nextHop();
+            }
+
+            if (nextHop != null && !nextHop.equals(routeEntry.nextHop())) {
+                // There was an existing nexthop for this prefix. This update
+                // supersedes that, so we need to remove the old flows for this
+                // prefix from the switches
+                executeRouteDelete(routeEntry);
+            }
+            if (nextHop != null && nextHop.equals(routeEntry.nextHop())) {
+                return;
+            }
+
+            if (routeEntry.nextHop().equals(LOCAL_NEXT_HOP)) {
+                // Route originated by SDN domain
+                // We don't handle these at the moment
+                log.debug("Own route {} to {}",
+                          routeEntry.prefix(), routeEntry.nextHop());
+                return;
+            }
+
+            executeRouteAdd(routeEntry);
+        }
+    }
+
+    /**
+     * Executes adding a route entry.
+     * <p/>
+     * Find out the egress Interface and MAC address of next hop router for
+     * this route entry. If the MAC address can not be found in ARP cache,
+     * then this prefix will be put in routesWaitingOnArp queue. Otherwise,
+     * new route intent will be created and installed.
+     *
+     * @param routeEntry the route entry to add
+     */
+    private void executeRouteAdd(RouteEntry routeEntry) {
+        log.debug("Executing route add: {}", routeEntry);
+
+        // See if we know the MAC address of the next hop
+        //MacAddress nextHopMacAddress =
+            //proxyArp.getMacAddress(routeEntry.getNextHop());
+        MacAddress nextHopMacAddress = null;
+        Set<Host> hosts = hostService.getHostsByIp(
+                routeEntry.nextHop().toPrefix());
+        if (!hosts.isEmpty()) {
+            // TODO how to handle if multiple hosts are returned?
+            nextHopMacAddress = hosts.iterator().next().mac();
+        }
+
+        if (nextHopMacAddress == null) {
+            routesWaitingOnArp.put(routeEntry.nextHop(), routeEntry);
+            //proxyArp.sendArpRequest(routeEntry.getNextHop(), this, true);
+            // TODO maybe just do this for every prefix anyway
+            hostService.startMonitoringIp(routeEntry.nextHop());
+            return;
+        }
+
+        addRouteIntentToNextHop(routeEntry.prefix(),
+                                routeEntry.nextHop(),
+                                nextHopMacAddress);
+    }
+
+    /**
+     * Adds a route intent given a prefix and a next hop IP address. This
+     * method will find the egress interface for the intent.
+     *
+     * @param prefix IP prefix of the route to add
+     * @param nextHopIpAddress IP address of the next hop
+     * @param nextHopMacAddress MAC address of the next hop
+     */
+    private void addRouteIntentToNextHop(IpPrefix prefix,
+                                         IpAddress nextHopIpAddress,
+                                         MacAddress nextHopMacAddress) {
+
+        // Find the attachment point (egress interface) of the next hop
+        Interface egressInterface;
+        if (configInfoService.getBgpPeers().containsKey(nextHopIpAddress)) {
+            // Route to a peer
+            log.debug("Route to peer {}", nextHopIpAddress);
+            BgpPeer peer =
+                configInfoService.getBgpPeers().get(nextHopIpAddress);
+            egressInterface =
+                interfaceService.getInterface(peer.connectPoint());
+        } else {
+            // Route to non-peer
+            log.debug("Route to non-peer {}", nextHopIpAddress);
+            egressInterface =
+                interfaceService.getMatchingInterface(nextHopIpAddress);
+            if (egressInterface == null) {
+                log.warn("No outgoing interface found for {}",
+                         nextHopIpAddress);
+                return;
+            }
+        }
+
+        doAddRouteIntent(prefix, egressInterface, nextHopMacAddress);
+    }
+
+    /**
+     * Installs a route intent for a prefix.
+     * <p/>
+     * Intent will match dst IP prefix and rewrite dst MAC address at all other
+     * border switches, then forward packets according to dst MAC address.
+     *
+     * @param prefix IP prefix from route
+     * @param egressInterface egress Interface connected to next hop router
+     * @param nextHopMacAddress MAC address of next hop router
+     */
+    private void doAddRouteIntent(IpPrefix prefix, Interface egressInterface,
+            MacAddress nextHopMacAddress) {
+        log.debug("Adding intent for prefix {}, next hop mac {}",
+                  prefix, nextHopMacAddress);
+
+        MultiPointToSinglePointIntent pushedIntent =
+            pushedRouteIntents.get(prefix);
+
+        // Just for testing.
+        if (pushedIntent != null) {
+            log.error("There should not be a pushed intent: {}", pushedIntent);
+        }
+
+        ConnectPoint egressPort = egressInterface.connectPoint();
+
+        Set<ConnectPoint> ingressPorts = new HashSet<>();
+
+        for (Interface intf : interfaceService.getInterfaces()) {
+            if (!intf.equals(egressInterface)) {
+                ConnectPoint srcPort = intf.connectPoint();
+                ingressPorts.add(srcPort);
+            }
+        }
+
+        // Match the destination IP prefix at the first hop
+        //PacketMatchBuilder builder = new PacketMatchBuilder();
+        //builder.setEtherType(Ethernet.TYPE_IPV4).setDstIpNet(prefix);
+        //PacketMatch packetMatch = builder.build();
+        TrafficSelector selector = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_IPV4)
+                .matchIPDst(prefix)
+                .build();
+
+        // Rewrite the destination MAC address
+        //ModifyDstMacAction modifyDstMacAction =
+                //new ModifyDstMacAction(nextHopMacAddress);
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                .setEthDst(nextHopMacAddress)
+                .build();
+
+        MultiPointToSinglePointIntent intent =
+                new MultiPointToSinglePointIntent(nextIntentId(),
+                        selector, treatment, ingressPorts, egressPort);
+
+        if (isElectedLeader && isActivatedLeader) {
+            log.debug("Intent installation: adding Intent for prefix: {}",
+                      prefix);
+            intentService.submit(intent);
+        }
+
+        // Maintain the Intent
+        pushedRouteIntents.put(prefix, intent);
+    }
+
+    /**
+     * Executes deleting a route entry.
+     * <p/>
+     * Removes prefix from InvertedRadixTree, if success, then try to delete
+     * the relative intent.
+     *
+     * @param routeEntry the route entry to delete
+     */
+    protected void processRouteDelete(RouteEntry routeEntry) {
+        synchronized (this) {
+            log.debug("Processing route delete: {}", routeEntry);
+            IpPrefix prefix = routeEntry.prefix();
+
+            // TODO check the change of logic here - remove doesn't check that
+            // the route entry was what we expected (and we can't do this
+            // concurrently)
+
+            if (bgpRoutes.remove(RouteEntry.createBinaryString(prefix))) {
+                //
+                // Only delete flows if an entry was actually removed from the
+                // tree. If no entry was removed, the <prefix, nexthop> wasn't
+                // there so it's probably already been removed and we don't
+                // need to do anything.
+                //
+                executeRouteDelete(routeEntry);
+            }
+
+            routesWaitingOnArp.remove(routeEntry.nextHop(), routeEntry);
+            // TODO cancel the request in the ARP manager as well
+        }
+    }
+
+    /**
+     * Executed deleting a route entry.
+     *
+     * @param routeEntry the route entry to delete
+     */
+    private void executeRouteDelete(RouteEntry routeEntry) {
+        log.debug("Executing route delete: {}", routeEntry);
+
+        IpPrefix prefix = routeEntry.prefix();
+
+        MultiPointToSinglePointIntent intent =
+            pushedRouteIntents.remove(prefix);
+
+        if (intent == null) {
+            log.debug("There is no intent in pushedRouteIntents to delete " +
+                      "for prefix: {}", prefix);
+        } else {
+            if (isElectedLeader && isActivatedLeader) {
+                log.debug("Intent installation: deleting Intent for prefix: {}",
+                          prefix);
+                intentService.withdraw(intent);
+            }
+        }
+    }
+
+    /**
+     * This method handles the prefixes which are waiting for ARP replies for
+     * MAC addresses of next hops.
+     *
+     * @param ipAddress next hop router IP address, for which we sent ARP
+     * request out
+     * @param macAddress MAC address which is relative to the ipAddress
+     */
+    //@Override
+    // TODO change name
+    public void arpResponse(IpAddress ipAddress, MacAddress macAddress) {
+        log.debug("Received ARP response: {} => {}", ipAddress, macAddress);
+
+         // We synchronize on this to prevent changes to the InvertedRadixTree
+         // while we're pushing intent. If the InvertedRadixTree changes, the
+         // InvertedRadixTree and intent could get out of sync.
+        synchronized (this) {
+
+            Set<RouteEntry> routesToPush =
+                routesWaitingOnArp.removeAll(ipAddress);
+
+            for (RouteEntry routeEntry : routesToPush) {
+                // These will always be adds
+                IpPrefix prefix = routeEntry.prefix();
+                String binaryString = RouteEntry.createBinaryString(prefix);
+                RouteEntry foundRouteEntry =
+                    bgpRoutes.getValueForExactKey(binaryString);
+                if (foundRouteEntry != null &&
+                    foundRouteEntry.nextHop().equals(routeEntry.nextHop())) {
+                    log.debug("Pushing prefix {} next hop {}",
+                              routeEntry.prefix(), routeEntry.nextHop());
+                    // We only push prefix flows if the prefix is still in the
+                    // InvertedRadixTree and the next hop is the same as our
+                    // update.
+                    // The prefix could have been removed while we were waiting
+                    // for the ARP, or the next hop could have changed.
+                    addRouteIntentToNextHop(prefix, ipAddress, macAddress);
+                } else {
+                    log.debug("Received ARP response, but {}/{} is no longer in"
+                            + " InvertedRadixTree", routeEntry.prefix(),
+                            routeEntry.nextHop());
+                }
+            }
+        }
+    }
+
+    /**
+     * Gets the SDN-IP routes.
+     *
+     * @return the SDN-IP routes
+     */
+    public Collection<RouteEntry> getRoutes() {
+        Iterator<KeyValuePair<RouteEntry>> it =
+                bgpRoutes.getKeyValuePairsForKeysStartingWith("").iterator();
+
+        List<RouteEntry> routes = new LinkedList<>();
+
+        while (it.hasNext()) {
+            KeyValuePair<RouteEntry> entry = it.next();
+            routes.add(entry.getValue());
+        }
+
+        return routes;
+    }
+
+    /**
+     * Generates a new unique intent ID.
+     *
+     * @return the new intent ID.
+     */
+    private IntentId nextIntentId() {
+        return new IntentId(intentId++);
+    }
+
+    /**
+     * Listener for host events.
+     */
+    class InternalHostListener implements HostListener {
+        @Override
+        public void event(HostEvent event) {
+            if (event.type() == HostEvent.Type.HOST_ADDED ||
+                    event.type() == HostEvent.Type.HOST_UPDATED) {
+                Host host = event.subject();
+                for (IpPrefix ip : host.ipAddresses()) {
+                    arpResponse(ip.toIpAddress(), host.mac());
+                }
+            }
+        }
+    }
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
index 25b13f1..3f94cba 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
@@ -9,7 +9,11 @@
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.onlab.onos.net.host.HostService;
 import org.onlab.onos.net.intent.IntentService;
+import org.onlab.onos.sdnip.RouteUpdate.Type;
+import org.onlab.onos.sdnip.bgp.BgpSessionManager;
 import org.onlab.onos.sdnip.config.SdnIpConfigReader;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
 import org.slf4j.Logger;
 
 /**
@@ -28,6 +32,8 @@
 
     private SdnIpConfigReader config;
     private PeerConnectivity peerConnectivity;
+    private Router router;
+    private BgpSessionManager bgpSessionManager;
 
     @Activate
     protected void activate() {
@@ -41,6 +47,17 @@
         peerConnectivity = new PeerConnectivity(config, interfaceService, intentService);
         peerConnectivity.start();
 
+        router = new Router(intentService, hostService, config, interfaceService);
+        router.start();
+
+        bgpSessionManager = new BgpSessionManager(router);
+        bgpSessionManager.startUp(2000); // TODO
+
+        // TODO need to disable link discovery on external ports
+
+        router.update(new RouteUpdate(Type.UPDATE, new RouteEntry(
+                IpPrefix.valueOf("172.16.20.0/24"),
+                IpAddress.valueOf("192.168.10.1"))));
     }
 
     @Deactivate
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpConstants.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpConstants.java
new file mode 100644
index 0000000..1703138
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpConstants.java
@@ -0,0 +1,368 @@
+package org.onlab.onos.sdnip.bgp;
+
+/**
+ * BGP related constants.
+ */
+public final class BgpConstants {
+    /**
+     * Default constructor.
+     * <p>
+     * The constructor is private to prevent creating an instance of
+     * this utility class.
+     */
+    private BgpConstants() {
+    }
+
+    /** BGP port number (RFC 4271). */
+    public static final int BGP_PORT = 179;
+
+    /** BGP version. */
+    public static final int BGP_VERSION = 4;
+
+    /** BGP OPEN message type. */
+    public static final int BGP_TYPE_OPEN = 1;
+
+    /** BGP UPDATE message type. */
+    public static final int BGP_TYPE_UPDATE = 2;
+
+    /** BGP NOTIFICATION message type. */
+    public static final int BGP_TYPE_NOTIFICATION = 3;
+
+    /** BGP KEEPALIVE message type. */
+    public static final int BGP_TYPE_KEEPALIVE = 4;
+
+    /** BGP Header Marker field length. */
+    public static final int BGP_HEADER_MARKER_LENGTH = 16;
+
+    /** BGP Header length. */
+    public static final int BGP_HEADER_LENGTH = 19;
+
+    /** BGP message maximum length. */
+    public static final int BGP_MESSAGE_MAX_LENGTH = 4096;
+
+    /** BGP OPEN message minimum length (BGP Header included). */
+    public static final int BGP_OPEN_MIN_LENGTH = 29;
+
+    /** BGP UPDATE message minimum length (BGP Header included). */
+    public static final int BGP_UPDATE_MIN_LENGTH = 23;
+
+    /** BGP NOTIFICATION message minimum length (BGP Header included). */
+    public static final int BGP_NOTIFICATION_MIN_LENGTH = 21;
+
+    /** BGP KEEPALIVE message expected length (BGP Header included). */
+    public static final int BGP_KEEPALIVE_EXPECTED_LENGTH = 19;
+
+    /** BGP KEEPALIVE messages transmitted per Hold interval. */
+    public static final int BGP_KEEPALIVE_PER_HOLD_INTERVAL = 3;
+
+    /** BGP KEEPALIVE messages minimum Holdtime (in seconds). */
+    public static final int BGP_KEEPALIVE_MIN_HOLDTIME = 3;
+
+    /** BGP KEEPALIVE messages minimum transmission interval (in seconds). */
+    public static final int BGP_KEEPALIVE_MIN_INTERVAL = 1;
+
+    /** BGP AS 0 (zero) value. See draft-ietf-idr-as0-06.txt Internet Draft. */
+    public static final long BGP_AS_0 = 0;
+
+    /**
+     * BGP UPDATE related constants.
+     */
+    public static final class Update {
+        /**
+         * Default constructor.
+         * <p>
+         * The constructor is private to prevent creating an instance of
+         * this utility class.
+         */
+        private Update() {
+        }
+
+        /**
+         * BGP UPDATE: ORIGIN related constants.
+         */
+        public static final class Origin {
+            /**
+             * Default constructor.
+             * <p>
+             * The constructor is private to prevent creating an instance of
+             * this utility class.
+             */
+            private Origin() {
+            }
+
+            /** BGP UPDATE Attributes Type Code ORIGIN. */
+            public static final int TYPE = 1;
+
+            /** BGP UPDATE Attributes Type Code ORIGIN length. */
+            public static final int LENGTH = 1;
+
+            /** BGP UPDATE ORIGIN: IGP. */
+            public static final int IGP = 0;
+
+            /** BGP UPDATE ORIGIN: EGP. */
+            public static final int EGP = 1;
+
+            /** BGP UPDATE ORIGIN: INCOMPLETE. */
+            public static final int INCOMPLETE = 2;
+        }
+
+        /**
+         * BGP UPDATE: AS_PATH related constants.
+         */
+        public static final class AsPath {
+            /**
+             * Default constructor.
+             * <p>
+             * The constructor is private to prevent creating an instance of
+             * this utility class.
+             */
+            private AsPath() {
+            }
+
+            /** BGP UPDATE Attributes Type Code AS_PATH. */
+            public static final int TYPE = 2;
+
+            /** BGP UPDATE AS_PATH Type: AS_SET. */
+            public static final int AS_SET = 1;
+
+            /** BGP UPDATE AS_PATH Type: AS_SEQUENCE. */
+            public static final int AS_SEQUENCE = 2;
+        }
+
+        /**
+         * BGP UPDATE: NEXT_HOP related constants.
+         */
+        public static final class NextHop {
+            /**
+             * Default constructor.
+             * <p>
+             * The constructor is private to prevent creating an instance of
+             * this utility class.
+             */
+            private NextHop() {
+            }
+
+            /** BGP UPDATE Attributes Type Code NEXT_HOP. */
+            public static final int TYPE = 3;
+
+            /** BGP UPDATE Attributes Type Code NEXT_HOP length. */
+            public static final int LENGTH = 4;
+        }
+
+        /**
+         * BGP UPDATE: MULTI_EXIT_DISC related constants.
+         */
+        public static final class MultiExitDisc {
+            /**
+             * Default constructor.
+             * <p>
+             * The constructor is private to prevent creating an instance of
+             * this utility class.
+             */
+            private MultiExitDisc() {
+            }
+
+            /** BGP UPDATE Attributes Type Code MULTI_EXIT_DISC. */
+            public static final int TYPE = 4;
+
+            /** BGP UPDATE Attributes Type Code MULTI_EXIT_DISC length. */
+            public static final int LENGTH = 4;
+
+            /** BGP UPDATE Attributes lowest MULTI_EXIT_DISC value. */
+            public static final int LOWEST_MULTI_EXIT_DISC = 0;
+        }
+
+        /**
+         * BGP UPDATE: LOCAL_PREF related constants.
+         */
+        public static final class LocalPref {
+            /**
+             * Default constructor.
+             * <p>
+             * The constructor is private to prevent creating an instance of
+             * this utility class.
+             */
+            private LocalPref() {
+            }
+
+            /** BGP UPDATE Attributes Type Code LOCAL_PREF. */
+            public static final int TYPE = 5;
+
+            /** BGP UPDATE Attributes Type Code LOCAL_PREF length. */
+            public static final int LENGTH = 4;
+        }
+
+        /**
+         * BGP UPDATE: ATOMIC_AGGREGATE related constants.
+         */
+        public static final class AtomicAggregate {
+            /**
+             * Default constructor.
+             * <p>
+             * The constructor is private to prevent creating an instance of
+             * this utility class.
+             */
+            private AtomicAggregate() {
+            }
+
+            /** BGP UPDATE Attributes Type Code ATOMIC_AGGREGATE. */
+            public static final int TYPE = 6;
+
+            /** BGP UPDATE Attributes Type Code ATOMIC_AGGREGATE length. */
+            public static final int LENGTH = 0;
+        }
+
+        /**
+         * BGP UPDATE: AGGREGATOR related constants.
+         */
+        public static final class Aggregator {
+            /**
+             * Default constructor.
+             * <p>
+             * The constructor is private to prevent creating an instance of
+             * this utility class.
+             */
+            private Aggregator() {
+            }
+
+            /** BGP UPDATE Attributes Type Code AGGREGATOR. */
+            public static final int TYPE = 7;
+
+            /** BGP UPDATE Attributes Type Code AGGREGATOR length. */
+            public static final int LENGTH = 6;
+        }
+    }
+
+    /**
+     * BGP NOTIFICATION related constants.
+     */
+    public static final class Notifications {
+        /**
+         * Default constructor.
+         * <p>
+         * The constructor is private to prevent creating an instance of
+         * this utility class.
+         */
+        private Notifications() {
+        }
+
+        /**
+         * BGP NOTIFICATION: Message Header Error constants.
+         */
+        public static final class MessageHeaderError {
+            /**
+             * Default constructor.
+             * <p>
+             * The constructor is private to prevent creating an instance of
+             * this utility class.
+             */
+            private MessageHeaderError() {
+            }
+
+            /** Message Header Error code. */
+            public static final int ERROR_CODE = 1;
+
+            /** Message Header Error subcode: Connection Not Synchronized. */
+            public static final int CONNECTION_NOT_SYNCHRONIZED = 1;
+
+            /** Message Header Error subcode: Bad Message Length. */
+            public static final int BAD_MESSAGE_LENGTH = 2;
+
+            /** Message Header Error subcode: Bad Message Type. */
+            public static final int BAD_MESSAGE_TYPE = 3;
+        }
+
+        /**
+         * BGP NOTIFICATION: OPEN Message Error constants.
+         */
+        public static final class OpenMessageError {
+            /**
+             * Default constructor.
+             * <p>
+             * The constructor is private to prevent creating an instance of
+             * this utility class.
+             */
+            private OpenMessageError() {
+            }
+
+            /** OPEN Message Error code. */
+            public static final int ERROR_CODE = 2;
+
+            /** OPEN Message Error subcode: Unsupported Version Number. */
+            public static final int UNSUPPORTED_VERSION_NUMBER = 1;
+
+            /** OPEN Message Error subcode: Bad PEER AS. */
+            public static final int BAD_PEER_AS = 2;
+
+            /** OPEN Message Error subcode: Unacceptable Hold Time. */
+            public static final int UNACCEPTABLE_HOLD_TIME = 6;
+        }
+
+        /**
+         * BGP NOTIFICATION: UPDATE Message Error constants.
+         */
+        public static final class UpdateMessageError {
+            /**
+             * Default constructor.
+             * <p>
+             * The constructor is private to prevent creating an instance of
+             * this utility class.
+             */
+            private UpdateMessageError() {
+            }
+
+            /** UPDATE Message Error code. */
+            public static final int ERROR_CODE = 3;
+
+            /** UPDATE Message Error subcode: Malformed Attribute List. */
+            public static final int MALFORMED_ATTRIBUTE_LIST = 1;
+
+            /** UPDATE Message Error subcode: Unrecognized Well-known Attribute. */
+            public static final int UNRECOGNIZED_WELL_KNOWN_ATTRIBUTE = 2;
+
+            /** UPDATE Message Error subcode: Missing Well-known Attribute. */
+            public static final int MISSING_WELL_KNOWN_ATTRIBUTE = 3;
+
+           /** UPDATE Message Error subcode: Attribute Flags Error. */
+            public static final int ATTRIBUTE_FLAGS_ERROR = 4;
+
+            /** UPDATE Message Error subcode: Attribute Length Error. */
+            public static final int ATTRIBUTE_LENGTH_ERROR = 5;
+
+            /** UPDATE Message Error subcode: Invalid ORIGIN Attribute. */
+            public static final int INVALID_ORIGIN_ATTRIBUTE = 6;
+
+            /** UPDATE Message Error subcode: Invalid NEXT_HOP Attribute. */
+            public static final int INVALID_NEXT_HOP_ATTRIBUTE = 8;
+
+            /** UPDATE Message Error subcode: Optional Attribute Error. Unused. */
+            public static final int OPTIONAL_ATTRIBUTE_ERROR = 9;
+
+            /** UPDATE Message Error subcode: Invalid Network Field. */
+            public static final int INVALID_NETWORK_FIELD = 10;
+
+            /** UPDATE Message Error subcode: Malformed AS_PATH. */
+            public static final int MALFORMED_AS_PATH = 11;
+        }
+
+        /**
+         * BGP NOTIFICATION: Hold Timer Expired constants.
+         */
+        public static final class HoldTimerExpired {
+            /**
+             * Default constructor.
+             * <p>
+             * The constructor is private to prevent creating an instance of
+             * this utility class.
+             */
+            private HoldTimerExpired() {
+            }
+
+            /** Hold Timer Expired code. */
+            public static final int ERROR_CODE = 4;
+        }
+
+        /** BGP NOTIFICATION message Error subcode: Unspecific. */
+        public static final int ERROR_SUBCODE_UNSPECIFIC = 0;
+    }
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpFrameDecoder.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpFrameDecoder.java
new file mode 100644
index 0000000..938d975
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpFrameDecoder.java
@@ -0,0 +1,162 @@
+package org.onlab.onos.sdnip.bgp;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.handler.codec.frame.FrameDecoder;
+import org.onlab.onos.sdnip.bgp.BgpConstants.Notifications.MessageHeaderError;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class for handling the decoding of the BGP messages.
+ */
+class BgpFrameDecoder extends FrameDecoder {
+    private static final Logger log =
+        LoggerFactory.getLogger(BgpFrameDecoder.class);
+
+    private final BgpSession bgpSession;
+
+    /**
+     * Constructor for a given BGP Session.
+     *
+     * @param bgpSession the BGP session state to use.
+     */
+    BgpFrameDecoder(BgpSession bgpSession) {
+        this.bgpSession = bgpSession;
+    }
+
+    @Override
+    protected Object decode(ChannelHandlerContext ctx,
+                            Channel channel,
+                            ChannelBuffer buf) throws Exception {
+        //
+        // NOTE: If we close the channel during the decoding, we might still
+        // see some incoming messages while the channel closing is completed.
+        //
+        if (bgpSession.isClosed()) {
+            return null;
+        }
+
+        log.trace("BGP Peer: decode(): remoteAddr = {} localAddr = {} " +
+                  "messageSize = {}",
+                  ctx.getChannel().getRemoteAddress(),
+                  ctx.getChannel().getLocalAddress(),
+                  buf.readableBytes());
+
+        // Test for minimum length of the BGP message
+        if (buf.readableBytes() < BgpConstants.BGP_HEADER_LENGTH) {
+            // No enough data received
+            return null;
+        }
+
+        //
+        // Mark the current buffer position in case we haven't received
+        // the whole message.
+        //
+        buf.markReaderIndex();
+
+        //
+        // Read and check the BGP message Marker field: it must be all ones
+        // (See RFC 4271, Section 4.1)
+        //
+        byte[] marker = new byte[BgpConstants.BGP_HEADER_MARKER_LENGTH];
+        buf.readBytes(marker);
+        for (int i = 0; i < marker.length; i++) {
+            if (marker[i] != (byte) 0xff) {
+                log.debug("BGP RX Error: invalid marker {} at position {}",
+                          marker[i], i);
+                //
+                // ERROR: Connection Not Synchronized
+                //
+                // Send NOTIFICATION and close the connection
+                int errorCode = MessageHeaderError.ERROR_CODE;
+                int errorSubcode =
+                    MessageHeaderError.CONNECTION_NOT_SYNCHRONIZED;
+                ChannelBuffer txMessage =
+                    bgpSession.prepareBgpNotification(errorCode, errorSubcode,
+                                                      null);
+                ctx.getChannel().write(txMessage);
+                bgpSession.closeChannel(ctx);
+                return null;
+            }
+        }
+
+        //
+        // Read and check the BGP message Length field
+        //
+        int length = buf.readUnsignedShort();
+        if ((length < BgpConstants.BGP_HEADER_LENGTH) ||
+            (length > BgpConstants.BGP_MESSAGE_MAX_LENGTH)) {
+            log.debug("BGP RX Error: invalid Length field {}. " +
+                      "Must be between {} and {}",
+                      length,
+                      BgpConstants.BGP_HEADER_LENGTH,
+                      BgpConstants.BGP_MESSAGE_MAX_LENGTH);
+            //
+            // ERROR: Bad Message Length
+            //
+            // Send NOTIFICATION and close the connection
+            ChannelBuffer txMessage =
+                bgpSession.prepareBgpNotificationBadMessageLength(length);
+            ctx.getChannel().write(txMessage);
+            bgpSession.closeChannel(ctx);
+            return null;
+        }
+
+        //
+        // Test whether the rest of the message is received:
+        // So far we have read the Marker (16 octets) and the
+        // Length (2 octets) fields.
+        //
+        int remainingMessageLen =
+            length - BgpConstants.BGP_HEADER_MARKER_LENGTH - 2;
+        if (buf.readableBytes() < remainingMessageLen) {
+            // No enough data received
+            buf.resetReaderIndex();
+            return null;
+        }
+
+        //
+        // Read the BGP message Type field, and process based on that type
+        //
+        int type = buf.readUnsignedByte();
+        remainingMessageLen--;      // Adjust after reading the type
+        ChannelBuffer message = buf.readBytes(remainingMessageLen);
+
+        //
+        // Process the remaining of the message based on the message type
+        //
+        switch (type) {
+        case BgpConstants.BGP_TYPE_OPEN:
+            bgpSession.processBgpOpen(ctx, message);
+            break;
+        case BgpConstants.BGP_TYPE_UPDATE:
+            bgpSession.processBgpUpdate(ctx, message);
+            break;
+        case BgpConstants.BGP_TYPE_NOTIFICATION:
+            bgpSession.processBgpNotification(ctx, message);
+            break;
+        case BgpConstants.BGP_TYPE_KEEPALIVE:
+            bgpSession.processBgpKeepalive(ctx, message);
+            break;
+        default:
+            //
+            // ERROR: Bad Message Type
+            //
+            // Send NOTIFICATION and close the connection
+            int errorCode = MessageHeaderError.ERROR_CODE;
+            int errorSubcode = MessageHeaderError.BAD_MESSAGE_TYPE;
+            ChannelBuffer data = ChannelBuffers.buffer(1);
+            data.writeByte(type);
+            ChannelBuffer txMessage =
+                bgpSession.prepareBgpNotification(errorCode, errorSubcode,
+                                                  data);
+            ctx.getChannel().write(txMessage);
+            bgpSession.closeChannel(ctx);
+            return null;
+        }
+        return null;
+    }
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpRouteEntry.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpRouteEntry.java
new file mode 100644
index 0000000..890328a
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpRouteEntry.java
@@ -0,0 +1,432 @@
+package org.onlab.onos.sdnip.bgp;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+import org.onlab.onos.sdnip.RouteEntry;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Represents a route in BGP.
+ */
+public class BgpRouteEntry extends RouteEntry {
+    private final BgpSession bgpSession; // The BGP Session the route was
+                                         // received on
+    private final byte origin;          // Route ORIGIN: IGP, EGP, INCOMPLETE
+    private final AsPath asPath;        // The AS Path
+    private final long localPref;       // The local preference for the route
+    private long multiExitDisc =
+        BgpConstants.Update.MultiExitDisc.LOWEST_MULTI_EXIT_DISC;
+
+    /**
+     * Class constructor.
+     *
+     * @param bgpSession the BGP Session the route was received on
+     * @param prefix the prefix of the route
+     * @param nextHop the next hop of the route
+     * @param origin the route origin: 0=IGP, 1=EGP, 2=INCOMPLETE
+     * @param asPath the AS path
+     * @param localPref the route local preference
+     */
+    public BgpRouteEntry(BgpSession bgpSession, IpPrefix prefix,
+                         IpAddress nextHop, byte origin,
+                         BgpRouteEntry.AsPath asPath, long localPref) {
+        super(prefix, nextHop);
+        this.bgpSession = checkNotNull(bgpSession);
+        this.origin = origin;
+        this.asPath = checkNotNull(asPath);
+        this.localPref = localPref;
+    }
+
+    /**
+     * Gets the BGP Session the route was received on.
+     *
+     * @return the BGP Session the route was received on
+     */
+    public BgpSession getBgpSession() {
+        return bgpSession;
+    }
+
+    /**
+     * Gets the route origin: 0=IGP, 1=EGP, 2=INCOMPLETE.
+     *
+     * @return the route origin: 0=IGP, 1=EGP, 2=INCOMPLETE
+     */
+    public byte getOrigin() {
+        return origin;
+    }
+
+    /**
+     * Gets the route AS path.
+     *
+     * @return the route AS path
+     */
+    public BgpRouteEntry.AsPath getAsPath() {
+        return asPath;
+    }
+
+    /**
+     * Gets the route local preference.
+     *
+     * @return the route local preference
+     */
+    public long getLocalPref() {
+        return localPref;
+    }
+
+    /**
+     * Gets the route MED (Multi-Exit Discriminator).
+     *
+     * @return the route MED (Multi-Exit Discriminator)
+     */
+    public long getMultiExitDisc() {
+        return multiExitDisc;
+    }
+
+    /**
+     * Sets the route MED (Multi-Exit Discriminator).
+     *
+     * @param multiExitDisc the route MED (Multi-Exit Discriminator) to set
+     */
+    void setMultiExitDisc(long multiExitDisc) {
+        this.multiExitDisc = multiExitDisc;
+    }
+
+    /**
+     * Tests whether the route is originated from the local AS.
+     * <p/>
+     * The route is considered originated from the local AS if the AS Path
+     * is empty or if it begins with an AS_SET.
+     *
+     * @return true if the route is originated from the local AS, otherwise
+     * false
+     */
+    boolean isLocalRoute() {
+        if (asPath.getPathSegments().isEmpty()) {
+            return true;
+        }
+        PathSegment firstPathSegment = asPath.getPathSegments().get(0);
+        if (firstPathSegment.getType() == BgpConstants.Update.AsPath.AS_SET) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Gets the BGP Neighbor AS number the route was received from.
+     * <p/>
+     * If the router is originated from the local AS, the return value is
+     * zero (BGP_AS_0).
+     *
+     * @return the BGP Neighbor AS number the route was received from.
+     */
+    long getNeighborAs() {
+        if (isLocalRoute()) {
+            return BgpConstants.BGP_AS_0;
+        }
+        PathSegment firstPathSegment = asPath.getPathSegments().get(0);
+        if (firstPathSegment.getSegmentAsNumbers().isEmpty()) {
+            // TODO: Shouldn't happen. Should check during the parsing.
+            return BgpConstants.BGP_AS_0;
+        }
+        return firstPathSegment.getSegmentAsNumbers().get(0);
+    }
+
+    /**
+     * Tests whether the AS Path contains a loop.
+     * <p/>
+     * The test is done by comparing whether the AS Path contains the
+     * local AS number.
+     *
+     * @param localAsNumber the local AS number to compare against
+     * @return true if the AS Path contains a loop, otherwise false
+     */
+    boolean hasAsPathLoop(long localAsNumber) {
+        for (PathSegment pathSegment : asPath.getPathSegments()) {
+            for (Long asNumber : pathSegment.getSegmentAsNumbers()) {
+                if (asNumber.equals(localAsNumber)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Compares this BGP route against another BGP route by using the
+     * BGP Decision Process.
+     * <p/>
+     * NOTE: The comparison needs to be performed only on routes that have
+     * same IP Prefix.
+     *
+     * @param other the BGP route to compare against
+     * @return true if this BGP route is better than the other BGP route
+     * or same, otherwise false
+     */
+    boolean isBetterThan(BgpRouteEntry other) {
+        if (this == other) {
+            return true;        // Return true if same route
+        }
+
+        // Compare the LOCAL_PREF values: larger is better
+        if (getLocalPref() != other.getLocalPref()) {
+            return (getLocalPref() > other.getLocalPref());
+        }
+
+        // Compare the AS number in the path: smaller is better
+        if (getAsPath().getAsPathLength() !=
+            other.getAsPath().getAsPathLength()) {
+            return getAsPath().getAsPathLength() <
+                other.getAsPath().getAsPathLength();
+        }
+
+        // Compare the Origin number: lower is better
+        if (getOrigin() != other.getOrigin()) {
+            return (getOrigin() < other.getOrigin());
+        }
+
+        // Compare the MED if the neighbor AS is same: larger is better
+        medLabel: {
+            boolean thisIsLocalRoute = isLocalRoute();
+            if (thisIsLocalRoute != other.isLocalRoute()) {
+                break medLabel;                 // AS number is different
+            }
+            if (!thisIsLocalRoute) {
+                long thisNeighborAs = getNeighborAs();
+                if (thisNeighborAs != other.getNeighborAs()) {
+                    break medLabel;             // AS number is different
+                }
+                if (thisNeighborAs == BgpConstants.BGP_AS_0) {
+                    break medLabel;             // Invalid AS number
+                }
+            }
+
+            // Compare the MED
+            if (getMultiExitDisc() != other.getMultiExitDisc()) {
+                return (getMultiExitDisc() > other.getMultiExitDisc());
+            }
+        }
+
+        // Compare the peer BGP ID: lower is better
+        IpAddress peerBgpId = getBgpSession().getRemoteBgpId();
+        IpAddress otherPeerBgpId = other.getBgpSession().getRemoteBgpId();
+        if (!peerBgpId.equals(otherPeerBgpId)) {
+            return (peerBgpId.compareTo(otherPeerBgpId) < 0);
+        }
+
+        // Compare the peer BGP address: lower is better
+        IpAddress peerAddress = getBgpSession().getRemoteIp4Address();
+        IpAddress otherPeerAddress =
+            other.getBgpSession().getRemoteIp4Address();
+        if (!peerAddress.equals(otherPeerAddress)) {
+            return (peerAddress.compareTo(otherPeerAddress) < 0);
+        }
+
+        return true;            // Routes are same. Shouldn't happen?
+    }
+
+    /**
+     * A class to represent AS Path Segment.
+     */
+    public static class PathSegment {
+        private final byte type;        // Segment type: AS_SET, AS_SEQUENCE
+        private final ArrayList<Long> segmentAsNumbers;   // Segment AS numbers
+
+        /**
+         * Constructor.
+         *
+         * @param type the Path Segment Type: 1=AS_SET, 2=AS_SEQUENCE
+         * @param segmentAsNumbers the Segment AS numbers
+         */
+        PathSegment(byte type, ArrayList<Long> segmentAsNumbers) {
+            this.type = type;
+            this.segmentAsNumbers = checkNotNull(segmentAsNumbers);
+        }
+
+        /**
+         * Gets the Path Segment Type: AS_SET, AS_SEQUENCE.
+         *
+         * @return the Path Segment Type: AS_SET, AS_SEQUENCE
+         */
+        public byte getType() {
+            return type;
+        }
+
+        /**
+         * Gets the Path Segment AS Numbers.
+         *
+         * @return the Path Segment AS Numbers
+         */
+        public ArrayList<Long> getSegmentAsNumbers() {
+            return segmentAsNumbers;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+
+            if (!(other instanceof PathSegment)) {
+                return false;
+            }
+
+            PathSegment otherPathSegment = (PathSegment) other;
+            return Objects.equals(this.type, otherPathSegment.type) &&
+                Objects.equals(this.segmentAsNumbers,
+                               otherPathSegment.segmentAsNumbers);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(type, segmentAsNumbers);
+        }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(getClass())
+                .add("type", this.type)
+                .add("segmentAsNumbers", this.segmentAsNumbers)
+                .toString();
+        }
+    }
+
+    /**
+     * A class to represent AS Path.
+     */
+    public static class AsPath {
+        private final ArrayList<PathSegment> pathSegments;
+        private final int asPathLength;         // Precomputed AS Path Length
+
+        /**
+         * Constructor.
+         *
+         * @param pathSegments the Path Segments of the Path
+         */
+         AsPath(ArrayList<PathSegment> pathSegments) {
+             this.pathSegments = checkNotNull(pathSegments);
+
+             //
+             // Precompute the AS Path Length:
+             // - AS_SET counts as 1
+             //
+             int pl = 0;
+             for (PathSegment pathSegment : pathSegments) {
+                 if (pathSegment.getType() ==
+                     BgpConstants.Update.AsPath.AS_SET) {
+                     pl++;
+                     continue;
+                 }
+                 pl += pathSegment.getSegmentAsNumbers().size();
+             }
+             asPathLength = pl;
+         }
+
+        /**
+         * Gets the AS Path Segments.
+         *
+         * @return the AS Path Segments
+         */
+        public ArrayList<PathSegment> getPathSegments() {
+            return pathSegments;
+        }
+
+        /**
+         * Gets the AS Path Length as considered by the BGP Decision Process.
+         *
+         * @return the AS Path Length as considered by the BGP Decision Process
+         */
+        int getAsPathLength() {
+            return asPathLength;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+
+            if (!(other instanceof AsPath)) {
+                return false;
+            }
+
+            AsPath otherAsPath = (AsPath) other;
+            return Objects.equals(this.pathSegments, otherAsPath.pathSegments);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(pathSegments);
+        }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(getClass())
+                .add("pathSegments", this.pathSegments)
+                .toString();
+        }
+    }
+
+    /**
+     * Compares whether two objects are equal.
+     * <p/>
+     * NOTE: The bgpSession field is excluded from the comparison.
+     *
+     * @return true if the two objects are equal, otherwise false.
+     */
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        //
+        // NOTE: Subclasses are considered as change of identity, hence
+        // equals() will return false if the class type doesn't match.
+        //
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        if (!super.equals(other)) {
+            return false;
+        }
+
+        // NOTE: The bgpSession field is excluded from the comparison
+        BgpRouteEntry otherRoute = (BgpRouteEntry) other;
+        return (this.origin == otherRoute.origin) &&
+            Objects.equals(this.asPath, otherRoute.asPath) &&
+            (this.localPref == otherRoute.localPref) &&
+            (this.multiExitDisc == otherRoute.multiExitDisc);
+    }
+
+    /**
+     * Computes the hash code.
+     * <p/>
+     * NOTE: We return the base class hash code to avoid expensive computation
+     *
+     * @return the object hash code
+     */
+    @Override
+    public int hashCode() {
+        return super.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+            .add("prefix", prefix())
+            .add("nextHop", nextHop())
+            .add("bgpId", bgpSession.getRemoteBgpId())
+            .add("origin", origin)
+            .add("asPath", asPath)
+            .add("localPref", localPref)
+            .add("multiExitDisc", multiExitDisc)
+            .toString();
+    }
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpSession.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpSession.java
new file mode 100644
index 0000000..0b39bb7
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpSession.java
@@ -0,0 +1,1840 @@
+package org.onlab.onos.sdnip.bgp;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelStateEvent;
+import org.jboss.netty.channel.SimpleChannelHandler;
+import org.jboss.netty.util.HashedWheelTimer;
+import org.jboss.netty.util.Timeout;
+import org.jboss.netty.util.Timer;
+import org.jboss.netty.util.TimerTask;
+import org.onlab.onos.sdnip.bgp.BgpConstants.Notifications;
+import org.onlab.onos.sdnip.bgp.BgpConstants.Notifications.HoldTimerExpired;
+import org.onlab.onos.sdnip.bgp.BgpConstants.Notifications.MessageHeaderError;
+import org.onlab.onos.sdnip.bgp.BgpConstants.Notifications.OpenMessageError;
+import org.onlab.onos.sdnip.bgp.BgpConstants.Notifications.UpdateMessageError;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class for handling the BGP peer sessions.
+ * There is one instance per each BGP peer session.
+ */
+public class BgpSession extends SimpleChannelHandler {
+    private static final Logger log =
+        LoggerFactory.getLogger(BgpSession.class);
+
+    private final BgpSessionManager bgpSessionManager;
+
+    // Local flag to indicate the session is closed.
+    // It is used to avoid the Netty's asynchronous closing of a channel.
+    private boolean isClosed = false;
+
+    private SocketAddress remoteAddress;        // Peer IP addr/port
+    private IpAddress remoteIp4Address;        // Peer IPv4 address
+    private int remoteBgpVersion;               // 1 octet
+    private long remoteAs;                      // 2 octets
+    private long remoteHoldtime;                // 2 octets
+    private IpAddress remoteBgpId;             // 4 octets -> IPv4 address
+    //
+    private SocketAddress localAddress;         // Local IP addr/port
+    private IpAddress localIp4Address;         // Local IPv4 address
+    private int localBgpVersion;                // 1 octet
+    private long localAs;                       // 2 octets
+    private long localHoldtime;                 // 2 octets
+    private IpAddress localBgpId;              // 4 octets -> IPv4 address
+    //
+    private long localKeepaliveInterval;        // Keepalive interval
+
+    // Timers state
+    private Timer timer = new HashedWheelTimer();
+    private volatile Timeout keepaliveTimeout;  // Periodic KEEPALIVE
+    private volatile Timeout sessionTimeout;    // Session timeout
+
+    // BGP RIB-IN routing entries from this peer
+    private ConcurrentMap<IpPrefix, BgpRouteEntry> bgpRibIn =
+        new ConcurrentHashMap<>();
+
+    /**
+     * Constructor for a given BGP Session Manager.
+     *
+     * @param bgpSessionManager the BGP Session Manager to use
+     */
+    BgpSession(BgpSessionManager bgpSessionManager) {
+        this.bgpSessionManager = bgpSessionManager;
+    }
+
+    /**
+     * Gets the BGP RIB-IN routing entries.
+     *
+     * @return the BGP RIB-IN routing entries
+     */
+    public Collection<BgpRouteEntry> getBgpRibIn() {
+        return bgpRibIn.values();
+    }
+
+    /**
+     * Finds a BGP routing entry in the BGP RIB-IN.
+     *
+     * @param prefix the prefix of the route to search for
+     * @return the BGP routing entry if found, otherwise null
+     */
+    public BgpRouteEntry findBgpRouteEntry(IpPrefix prefix) {
+        return bgpRibIn.get(prefix);
+    }
+
+    /**
+     * Gets the BGP session remote address.
+     *
+     * @return the BGP session remote address
+     */
+    public SocketAddress getRemoteAddress() {
+        return remoteAddress;
+    }
+
+    /**
+     * Gets the BGP session remote IPv4 address.
+     *
+     * @return the BGP session remote IPv4 address
+     */
+    public IpAddress getRemoteIp4Address() {
+        return remoteIp4Address;
+    }
+
+    /**
+     * Gets the BGP session remote BGP version.
+     *
+     * @return the BGP session remote BGP version
+     */
+    public int getRemoteBgpVersion() {
+        return remoteBgpVersion;
+    }
+
+    /**
+     * Gets the BGP session remote AS number.
+     *
+     * @return the BGP session remote AS number
+     */
+    public long getRemoteAs() {
+        return remoteAs;
+    }
+
+    /**
+     * Gets the BGP session remote Holdtime.
+     *
+     * @return the BGP session remote Holdtime
+     */
+    public long getRemoteHoldtime() {
+        return remoteHoldtime;
+    }
+
+    /**
+     * Gets the BGP session remote BGP Identifier as an IPv4 address.
+     *
+     * @return the BGP session remote BGP Identifier as an IPv4 address
+     */
+    public IpAddress getRemoteBgpId() {
+        return remoteBgpId;
+    }
+
+    /**
+     * Gets the BGP session local address.
+     *
+     * @return the BGP session local address
+     */
+    public SocketAddress getLocalAddress() {
+        return localAddress;
+    }
+
+    /**
+     * Gets the BGP session local BGP version.
+     *
+     * @return the BGP session local BGP version
+     */
+    public int getLocalBgpVersion() {
+        return localBgpVersion;
+    }
+
+    /**
+     * Gets the BGP session local AS number.
+     *
+     * @return the BGP session local AS number
+     */
+    public long getLocalAs() {
+        return localAs;
+    }
+
+    /**
+     * Gets the BGP session local Holdtime.
+     *
+     * @return the BGP session local Holdtime
+     */
+    public long getLocalHoldtime() {
+        return localHoldtime;
+    }
+
+    /**
+     * Gets the BGP session local BGP Identifier as an IPv4 address.
+     *
+     * @return the BGP session local BGP Identifier as an IPv4 address
+     */
+    public IpAddress getLocalBgpId() {
+        return localBgpId;
+    }
+
+    /**
+     * Tests whether the session is closed.
+     * <p/>
+     * NOTE: We use this method to avoid the Netty's asynchronous closing
+     * of a channel.
+     *
+     * @param return true if the session is closed
+     */
+    boolean isClosed() {
+        return isClosed;
+    }
+
+    /**
+     * Closes the channel.
+     *
+     * @param ctx the Channel Handler Context
+     */
+    void closeChannel(ChannelHandlerContext ctx) {
+        isClosed = true;
+        timer.stop();
+        ctx.getChannel().close();
+    }
+
+    @Override
+    public void channelConnected(ChannelHandlerContext ctx,
+                                 ChannelStateEvent channelEvent) {
+        localAddress = ctx.getChannel().getLocalAddress();
+        remoteAddress = ctx.getChannel().getRemoteAddress();
+
+        // Assign the local and remote IPv4 addresses
+        InetAddress inetAddr;
+        if (localAddress instanceof InetSocketAddress) {
+            inetAddr = ((InetSocketAddress) localAddress).getAddress();
+            localIp4Address = IpAddress.valueOf(inetAddr.getAddress());
+        }
+        if (remoteAddress instanceof InetSocketAddress) {
+            inetAddr = ((InetSocketAddress) remoteAddress).getAddress();
+            remoteIp4Address = IpAddress.valueOf(inetAddr.getAddress());
+        }
+
+        log.debug("BGP Session Connected from {} on {}",
+                  remoteAddress, localAddress);
+        if (!bgpSessionManager.peerConnected(this)) {
+            log.debug("Cannot setup BGP Session Connection from {}. Closing...",
+                      remoteAddress);
+            ctx.getChannel().close();
+        }
+    }
+
+    @Override
+    public void channelDisconnected(ChannelHandlerContext ctx,
+                                    ChannelStateEvent channelEvent) {
+        log.debug("BGP Session Disconnected from {} on {}",
+                  ctx.getChannel().getRemoteAddress(),
+                  ctx.getChannel().getLocalAddress());
+
+        //
+        // Withdraw the routes advertised by this BGP peer
+        //
+        // NOTE: We must initialize the RIB-IN before propagating the withdraws
+        // for further processing. Otherwise, the BGP Decision Process
+        // will use those routes again.
+        //
+        Collection<BgpRouteEntry> deletedRoutes = bgpRibIn.values();
+        bgpRibIn = new ConcurrentHashMap<>();
+
+        // Push the updates to the BGP Merged RIB
+        BgpSessionManager.BgpRouteSelector bgpRouteSelector =
+            bgpSessionManager.getBgpRouteSelector();
+        Collection<BgpRouteEntry> addedRoutes = Collections.emptyList();
+        bgpRouteSelector.routeUpdates(this, addedRoutes, deletedRoutes);
+
+        bgpSessionManager.peerDisconnected(this);
+    }
+
+    /**
+     * Processes BGP OPEN message.
+     *
+     * @param ctx the Channel Handler Context
+     * @param message the message to process
+     */
+    void processBgpOpen(ChannelHandlerContext ctx, ChannelBuffer message) {
+        int minLength =
+            BgpConstants.BGP_OPEN_MIN_LENGTH - BgpConstants.BGP_HEADER_LENGTH;
+        if (message.readableBytes() < minLength) {
+            log.debug("BGP RX OPEN Error from {}: " +
+                      "Message length {} too short. Must be at least {}",
+                      remoteAddress, message.readableBytes(), minLength);
+            //
+            // ERROR: Bad Message Length
+            //
+            // Send NOTIFICATION and close the connection
+            ChannelBuffer txMessage = prepareBgpNotificationBadMessageLength(
+                message.readableBytes() + BgpConstants.BGP_HEADER_LENGTH);
+            ctx.getChannel().write(txMessage);
+            closeChannel(ctx);
+            return;
+        }
+
+        //
+        // Parse the OPEN message
+        //
+        // Remote BGP version
+        remoteBgpVersion = message.readUnsignedByte();
+        if (remoteBgpVersion != BgpConstants.BGP_VERSION) {
+            log.debug("BGP RX OPEN Error from {}: " +
+                      "Unsupported BGP version {}. Should be {}",
+                      remoteAddress, remoteBgpVersion,
+                      BgpConstants.BGP_VERSION);
+            //
+            // ERROR: Unsupported Version Number
+            //
+            // Send NOTIFICATION and close the connection
+            int errorCode = OpenMessageError.ERROR_CODE;
+            int errorSubcode = OpenMessageError.UNSUPPORTED_VERSION_NUMBER;
+            ChannelBuffer data = ChannelBuffers.buffer(2);
+            data.writeShort(BgpConstants.BGP_VERSION);
+            ChannelBuffer txMessage =
+                prepareBgpNotification(errorCode, errorSubcode, data);
+            ctx.getChannel().write(txMessage);
+            closeChannel(ctx);
+            return;
+        }
+
+        // Remote AS number
+        remoteAs = message.readUnsignedShort();
+        //
+        // Verify that the AS number is same for all other BGP Sessions
+        // NOTE: This check applies only for our use-case where all BGP
+        // sessions are iBGP.
+        //
+        for (BgpSession bgpSession : bgpSessionManager.getBgpSessions()) {
+            if (remoteAs != bgpSession.getRemoteAs()) {
+                log.debug("BGP RX OPEN Error from {}: Bad Peer AS {}. " +
+                          "Expected {}",
+                          remoteAddress, remoteAs, bgpSession.getRemoteAs());
+                //
+                // ERROR: Bad Peer AS
+                //
+                // Send NOTIFICATION and close the connection
+                int errorCode = OpenMessageError.ERROR_CODE;
+                int errorSubcode = OpenMessageError.BAD_PEER_AS;
+                ChannelBuffer txMessage =
+                    prepareBgpNotification(errorCode, errorSubcode, null);
+                ctx.getChannel().write(txMessage);
+                closeChannel(ctx);
+                return;
+            }
+        }
+
+        // Remote Hold Time
+        remoteHoldtime = message.readUnsignedShort();
+        if ((remoteHoldtime != 0) &&
+            (remoteHoldtime < BgpConstants.BGP_KEEPALIVE_MIN_HOLDTIME)) {
+            log.debug("BGP RX OPEN Error from {}: " +
+                      "Unacceptable Hold Time field {}. " +
+                      "Should be 0 or at least {}",
+                      remoteAddress, remoteHoldtime,
+                      BgpConstants.BGP_KEEPALIVE_MIN_HOLDTIME);
+            //
+            // ERROR: Unacceptable Hold Time
+            //
+            // Send NOTIFICATION and close the connection
+            int errorCode = OpenMessageError.ERROR_CODE;
+            int errorSubcode = OpenMessageError.UNACCEPTABLE_HOLD_TIME;
+            ChannelBuffer txMessage =
+                prepareBgpNotification(errorCode, errorSubcode, null);
+            ctx.getChannel().write(txMessage);
+            closeChannel(ctx);
+            return;
+        }
+
+        // Remote BGP Identifier
+        remoteBgpId = IpAddress.valueOf((int) message.readUnsignedInt());
+
+        // Optional Parameters
+        int optParamLen = message.readUnsignedByte();
+        if (message.readableBytes() < optParamLen) {
+            log.debug("BGP RX OPEN Error from {}: " +
+                      "Invalid Optional Parameter Length field {}. " +
+                      "Remaining Optional Parameters {}",
+                      remoteAddress, optParamLen, message.readableBytes());
+            //
+            // ERROR: Invalid Optional Parameter Length field: Unspecific
+            //
+            // Send NOTIFICATION and close the connection
+            int errorCode = OpenMessageError.ERROR_CODE;
+            int errorSubcode = Notifications.ERROR_SUBCODE_UNSPECIFIC;
+            ChannelBuffer txMessage =
+                prepareBgpNotification(errorCode, errorSubcode, null);
+            ctx.getChannel().write(txMessage);
+            closeChannel(ctx);
+            return;
+        }
+        // TODO: Parse the optional parameters (if needed)
+        message.readBytes(optParamLen);             // NOTE: data ignored
+
+        //
+        // Copy some of the remote peer's state/setup to the local setup:
+        //  - BGP version
+        //  - AS number (NOTE: the peer setup is always iBGP)
+        //  - Holdtime
+        // Also, assign the local BGP ID based on the local setup
+        //
+        localBgpVersion = remoteBgpVersion;
+        localAs = remoteAs;
+        localHoldtime = remoteHoldtime;
+        localBgpId = bgpSessionManager.getMyBgpId();
+
+        // Set the Keepalive interval
+        if (localHoldtime == 0) {
+            localKeepaliveInterval = 0;
+        } else {
+            localKeepaliveInterval = Math.max(localHoldtime /
+                         BgpConstants.BGP_KEEPALIVE_PER_HOLD_INTERVAL,
+                         BgpConstants.BGP_KEEPALIVE_MIN_INTERVAL);
+        }
+
+        log.debug("BGP RX OPEN message from {}: " +
+                  "BGPv{} AS {} BGP-ID {} Holdtime {}",
+                  remoteAddress, remoteBgpVersion, remoteAs,
+                  remoteBgpId, remoteHoldtime);
+
+        // Send my OPEN followed by KEEPALIVE
+        ChannelBuffer txMessage = prepareBgpOpen();
+        ctx.getChannel().write(txMessage);
+        //
+        txMessage = prepareBgpKeepalive();
+        ctx.getChannel().write(txMessage);
+
+        // Start the KEEPALIVE timer
+        restartKeepaliveTimer(ctx);
+
+        // Start the Session Timeout timer
+        restartSessionTimeoutTimer(ctx);
+    }
+
+    /**
+     * Processes BGP UPDATE message.
+     *
+     * @param ctx the Channel Handler Context
+     * @param message the message to process
+     */
+    void processBgpUpdate(ChannelHandlerContext ctx, ChannelBuffer message) {
+        Collection<BgpRouteEntry> addedRoutes = null;
+        Map<IpPrefix, BgpRouteEntry> deletedRoutes = new HashMap<>();
+
+        int minLength =
+            BgpConstants.BGP_UPDATE_MIN_LENGTH - BgpConstants.BGP_HEADER_LENGTH;
+        if (message.readableBytes() < minLength) {
+            log.debug("BGP RX UPDATE Error from {}: " +
+                      "Message length {} too short. Must be at least {}",
+                      remoteAddress, message.readableBytes(), minLength);
+            //
+            // ERROR: Bad Message Length
+            //
+            // Send NOTIFICATION and close the connection
+            ChannelBuffer txMessage = prepareBgpNotificationBadMessageLength(
+                message.readableBytes() + BgpConstants.BGP_HEADER_LENGTH);
+            ctx.getChannel().write(txMessage);
+            closeChannel(ctx);
+            return;
+        }
+
+        log.debug("BGP RX UPDATE message from {}", remoteAddress);
+
+        //
+        // Parse the UPDATE message
+        //
+
+        //
+        // Parse the Withdrawn Routes
+        //
+        int withdrawnRoutesLength = message.readUnsignedShort();
+        if (withdrawnRoutesLength > message.readableBytes()) {
+            // ERROR: Malformed Attribute List
+            actionsBgpUpdateMalformedAttributeList(ctx);
+            return;
+        }
+        Collection<IpPrefix> withdrawnPrefixes = null;
+        try {
+            withdrawnPrefixes = parsePackedPrefixes(withdrawnRoutesLength,
+                                                    message);
+        } catch (BgpParseException e) {
+            // ERROR: Invalid Network Field
+            log.debug("Exception parsing Withdrawn Prefixes from BGP peer {}: ",
+                      remoteBgpId, e);
+            actionsBgpUpdateInvalidNetworkField(ctx);
+            return;
+        }
+        for (IpPrefix prefix : withdrawnPrefixes) {
+            log.debug("BGP RX UPDATE message WITHDRAWN from {}: {}",
+                      remoteAddress, prefix);
+            BgpRouteEntry bgpRouteEntry = bgpRibIn.get(prefix);
+            if (bgpRouteEntry != null) {
+                deletedRoutes.put(prefix, bgpRouteEntry);
+            }
+        }
+
+        //
+        // Parse the Path Attributes
+        //
+        try {
+            addedRoutes = parsePathAttributes(ctx, message);
+        } catch (BgpParseException e) {
+            log.debug("Exception parsing Path Attributes from BGP peer {}: ",
+                      remoteBgpId, e);
+            // NOTE: The session was already closed, so nothing else to do
+            return;
+        }
+        // Ignore WITHDRAWN routes that are ADDED
+        for (BgpRouteEntry bgpRouteEntry : addedRoutes) {
+            deletedRoutes.remove(bgpRouteEntry.prefix());
+        }
+
+        // Update the BGP RIB-IN
+        for (BgpRouteEntry bgpRouteEntry : deletedRoutes.values()) {
+            bgpRibIn.remove(bgpRouteEntry.prefix());
+        }
+        for (BgpRouteEntry bgpRouteEntry : addedRoutes) {
+            bgpRibIn.put(bgpRouteEntry.prefix(), bgpRouteEntry);
+        }
+
+        // Push the updates to the BGP Merged RIB
+        BgpSessionManager.BgpRouteSelector bgpRouteSelector =
+            bgpSessionManager.getBgpRouteSelector();
+        bgpRouteSelector.routeUpdates(this, addedRoutes,
+                                      deletedRoutes.values());
+
+        // Start the Session Timeout timer
+        restartSessionTimeoutTimer(ctx);
+    }
+
+    /**
+     * Parse BGP Path Attributes from the BGP UPDATE message.
+     *
+     * @param ctx the Channel Handler Context
+     * @param message the message to parse
+     * @return a collection of the result BGP Route Entries
+     * @throws BgpParseException
+     */
+    private Collection<BgpRouteEntry> parsePathAttributes(
+                                ChannelHandlerContext ctx,
+                                ChannelBuffer message)
+        throws BgpParseException {
+        Map<IpPrefix, BgpRouteEntry> addedRoutes = new HashMap<>();
+
+        //
+        // Parsed values
+        //
+        Short origin = -1;                      // Mandatory
+        BgpRouteEntry.AsPath asPath = null;     // Mandatory
+        IpAddress nextHop = null;              // Mandatory
+        long multiExitDisc =                    // Optional
+            BgpConstants.Update.MultiExitDisc.LOWEST_MULTI_EXIT_DISC;
+        Long localPref = null;                  // Mandatory
+        Long aggregatorAsNumber = null;         // Optional: unused
+        IpAddress aggregatorIpAddress = null;  // Optional: unused
+
+        //
+        // Get and verify the Path Attributes Length
+        //
+        int pathAttributeLength = message.readUnsignedShort();
+        if (pathAttributeLength > message.readableBytes()) {
+            // ERROR: Malformed Attribute List
+            actionsBgpUpdateMalformedAttributeList(ctx);
+            String errorMsg = "Malformed Attribute List";
+            throw new BgpParseException(errorMsg);
+        }
+        if (pathAttributeLength == 0) {
+            return addedRoutes.values();
+        }
+
+        //
+        // Parse the Path Attributes
+        //
+        int pathAttributeEnd = message.readerIndex() + pathAttributeLength;
+        while (message.readerIndex() < pathAttributeEnd) {
+            int attrFlags = message.readUnsignedByte();
+            if (message.readerIndex() >= pathAttributeEnd) {
+                // ERROR: Malformed Attribute List
+                actionsBgpUpdateMalformedAttributeList(ctx);
+                String errorMsg = "Malformed Attribute List";
+                throw new BgpParseException(errorMsg);
+            }
+            int attrTypeCode = message.readUnsignedByte();
+
+            // The Attribute Flags
+            boolean optionalBit =       ((0x80 & attrFlags) != 0);
+            boolean transitiveBit =     ((0x40 & attrFlags) != 0);
+            boolean partialBit =        ((0x20 & attrFlags) != 0);
+            boolean extendedLengthBit = ((0x10 & attrFlags) != 0);
+
+            // The Attribute Length
+            int attrLen = 0;
+            int attrLenOctets = 1;
+            if (extendedLengthBit) {
+                attrLenOctets = 2;
+            }
+            if (message.readerIndex() + attrLenOctets > pathAttributeEnd) {
+                // ERROR: Malformed Attribute List
+                actionsBgpUpdateMalformedAttributeList(ctx);
+                String errorMsg = "Malformed Attribute List";
+                throw new BgpParseException(errorMsg);
+            }
+            if (extendedLengthBit) {
+                attrLen = message.readUnsignedShort();
+            } else {
+                attrLen = message.readUnsignedByte();
+            }
+            if (message.readerIndex() + attrLen > pathAttributeEnd) {
+                // ERROR: Malformed Attribute List
+                actionsBgpUpdateMalformedAttributeList(ctx);
+                String errorMsg = "Malformed Attribute List";
+                throw new BgpParseException(errorMsg);
+            }
+
+            //
+            // Verify the Attribute Flags
+            //
+            verifyBgpUpdateAttributeFlags(ctx, attrTypeCode, attrLen,
+                                          attrFlags, message);
+
+            //
+            // Extract the Attribute Value based on the Attribute Type Code
+            //
+            switch (attrTypeCode) {
+
+            case BgpConstants.Update.Origin.TYPE:
+                // Attribute Type Code ORIGIN
+                origin = parseAttributeTypeOrigin(ctx, attrTypeCode, attrLen,
+                                                  attrFlags, message);
+                break;
+
+            case BgpConstants.Update.AsPath.TYPE:
+                // Attribute Type Code AS_PATH
+                asPath = parseAttributeTypeAsPath(ctx, attrTypeCode, attrLen,
+                                                  attrFlags, message);
+                break;
+
+            case BgpConstants.Update.NextHop.TYPE:
+                // Attribute Type Code NEXT_HOP
+                nextHop = parseAttributeTypeNextHop(ctx, attrTypeCode, attrLen,
+                                                    attrFlags, message);
+                break;
+
+            case BgpConstants.Update.MultiExitDisc.TYPE:
+                // Attribute Type Code MULTI_EXIT_DISC
+                multiExitDisc =
+                    parseAttributeTypeMultiExitDisc(ctx, attrTypeCode, attrLen,
+                                                    attrFlags, message);
+                break;
+
+            case BgpConstants.Update.LocalPref.TYPE:
+                // Attribute Type Code LOCAL_PREF
+                localPref =
+                    parseAttributeTypeLocalPref(ctx, attrTypeCode, attrLen,
+                                                attrFlags, message);
+                break;
+
+            case BgpConstants.Update.AtomicAggregate.TYPE:
+                // Attribute Type Code ATOMIC_AGGREGATE
+                parseAttributeTypeAtomicAggregate(ctx, attrTypeCode, attrLen,
+                                                  attrFlags, message);
+                // Nothing to do: this attribute is primarily informational
+                break;
+
+            case BgpConstants.Update.Aggregator.TYPE:
+                // Attribute Type Code AGGREGATOR
+                Pair<Long, IpAddress> aggregator =
+                    parseAttributeTypeAggregator(ctx, attrTypeCode, attrLen,
+                                                 attrFlags, message);
+                aggregatorAsNumber = aggregator.getLeft();
+                aggregatorIpAddress = aggregator.getRight();
+                break;
+
+            default:
+                // TODO: Parse any new Attribute Types if needed
+                if (!optionalBit) {
+                    // ERROR: Unrecognized Well-known Attribute
+                    actionsBgpUpdateUnrecognizedWellKnownAttribute(
+                        ctx, attrTypeCode, attrLen, attrFlags, message);
+                    String errorMsg = "Unrecognized Well-known Attribute: " +
+                        attrTypeCode;
+                    throw new BgpParseException(errorMsg);
+                }
+
+                // Skip the data from the unrecognized attribute
+                log.debug("BGP RX UPDATE message from {}: " +
+                          "Unrecognized Attribute Type {}",
+                          remoteAddress, attrTypeCode);
+                message.skipBytes(attrLen);
+                break;
+            }
+        }
+
+        //
+        // Verify the Well-known Attributes
+        //
+        verifyBgpUpdateWellKnownAttributes(ctx, origin, asPath, nextHop,
+                                           localPref);
+
+        //
+        // Parse the NLRI (Network Layer Reachability Information)
+        //
+        Collection<IpPrefix> addedPrefixes = null;
+        int nlriLength = message.readableBytes();
+        try {
+            addedPrefixes = parsePackedPrefixes(nlriLength, message);
+        } catch (BgpParseException e) {
+            // ERROR: Invalid Network Field
+            log.debug("Exception parsing NLRI from BGP peer {}: ",
+                      remoteBgpId, e);
+            actionsBgpUpdateInvalidNetworkField(ctx);
+            // Rethrow the exception
+            throw e;
+        }
+
+        // Generate the added routes
+        for (IpPrefix prefix : addedPrefixes) {
+            BgpRouteEntry bgpRouteEntry =
+                new BgpRouteEntry(this, prefix, nextHop,
+                                  origin.byteValue(), asPath, localPref);
+            bgpRouteEntry.setMultiExitDisc(multiExitDisc);
+            if (bgpRouteEntry.hasAsPathLoop(localAs)) {
+                log.debug("BGP RX UPDATE message IGNORED from {}: {} " +
+                          "nextHop {}: contains AS Path loop",
+                          remoteAddress, prefix, nextHop);
+                continue;
+            } else {
+                log.debug("BGP RX UPDATE message ADDED from {}: {} nextHop {}",
+                          remoteAddress, prefix, nextHop);
+            }
+            addedRoutes.put(prefix, bgpRouteEntry);
+        }
+
+        return addedRoutes.values();
+    }
+
+    /**
+     * Verifies BGP UPDATE Well-known Attributes.
+     *
+     * @param ctx the Channel Handler Context
+     * @param origin the ORIGIN well-known mandatory attribute
+     * @param asPath the AS_PATH well-known mandatory attribute
+     * @param nextHop the NEXT_HOP well-known mandatory attribute
+     * @param localPref the LOCAL_PREF required attribute
+     * @throws BgpParseException
+     */
+    private void verifyBgpUpdateWellKnownAttributes(
+                        ChannelHandlerContext ctx,
+                        Short origin,
+                        BgpRouteEntry.AsPath asPath,
+                        IpAddress nextHop,
+                        Long localPref)
+        throws BgpParseException {
+        //
+        // Check for Missing Well-known Attributes
+        //
+        if ((origin == null) || (origin == -1)) {
+            // Missing Attribute Type Code ORIGIN
+            int type = BgpConstants.Update.Origin.TYPE;
+            actionsBgpUpdateMissingWellKnownAttribute(ctx, type);
+            String errorMsg = "Missing Well-known Attribute: ORIGIN";
+            throw new BgpParseException(errorMsg);
+        }
+        if (asPath == null) {
+            // Missing Attribute Type Code AS_PATH
+            int type = BgpConstants.Update.AsPath.TYPE;
+            actionsBgpUpdateMissingWellKnownAttribute(ctx, type);
+            String errorMsg = "Missing Well-known Attribute: AS_PATH";
+            throw new BgpParseException(errorMsg);
+        }
+        if (nextHop == null) {
+            // Missing Attribute Type Code NEXT_HOP
+            int type = BgpConstants.Update.NextHop.TYPE;
+            actionsBgpUpdateMissingWellKnownAttribute(ctx, type);
+            String errorMsg = "Missing Well-known Attribute: NEXT_HOP";
+            throw new BgpParseException(errorMsg);
+        }
+        if (localPref == null) {
+            // Missing Attribute Type Code LOCAL_PREF
+            // NOTE: Required for iBGP
+            int type = BgpConstants.Update.LocalPref.TYPE;
+            actionsBgpUpdateMissingWellKnownAttribute(ctx, type);
+            String errorMsg = "Missing Well-known Attribute: LOCAL_PREF";
+            throw new BgpParseException(errorMsg);
+        }
+    }
+
+    /**
+     * Verifies the BGP UPDATE Attribute Flags.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message to parse
+     * @throws BgpParseException
+     */
+    private void verifyBgpUpdateAttributeFlags(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message)
+        throws BgpParseException {
+
+        //
+        // Assign the Attribute Type Name and the Well-known flag
+        //
+        String typeName = "UNKNOWN";
+        boolean isWellKnown = false;
+        switch (attrTypeCode) {
+        case BgpConstants.Update.Origin.TYPE:
+            isWellKnown = true;
+            typeName = "ORIGIN";
+            break;
+        case BgpConstants.Update.AsPath.TYPE:
+            isWellKnown = true;
+            typeName = "AS_PATH";
+            break;
+        case BgpConstants.Update.NextHop.TYPE:
+            isWellKnown = true;
+            typeName = "NEXT_HOP";
+            break;
+        case BgpConstants.Update.MultiExitDisc.TYPE:
+            isWellKnown = false;
+            typeName = "MULTI_EXIT_DISC";
+            break;
+        case BgpConstants.Update.LocalPref.TYPE:
+            isWellKnown = true;
+            typeName = "LOCAL_PREF";
+            break;
+        case BgpConstants.Update.AtomicAggregate.TYPE:
+            isWellKnown = true;
+            typeName = "ATOMIC_AGGREGATE";
+            break;
+        case BgpConstants.Update.Aggregator.TYPE:
+            isWellKnown = false;
+            typeName = "AGGREGATOR";
+            break;
+        default:
+            isWellKnown = false;
+            typeName = "UNKNOWN(" + attrTypeCode + ")";
+            break;
+        }
+
+        //
+        // Verify the Attribute Flags
+        //
+        boolean optionalBit =       ((0x80 & attrFlags) != 0);
+        boolean transitiveBit =     ((0x40 & attrFlags) != 0);
+        boolean partialBit =        ((0x20 & attrFlags) != 0);
+        if ((isWellKnown && optionalBit) ||
+            (isWellKnown && (!transitiveBit)) ||
+            (isWellKnown && partialBit) ||
+            (optionalBit && (!transitiveBit) && partialBit)) {
+            //
+            // ERROR: The Optional bit cannot be set for Well-known attributes
+            // ERROR: The Transtive bit MUST be 1 for well-known attributes
+            // ERROR: The Partial bit MUST be 0 for well-known attributes
+            // ERROR: The Partial bit MUST be 0 for optional non-transitive
+            //        attributes
+            //
+            actionsBgpUpdateAttributeFlagsError(
+                ctx, attrTypeCode, attrLen, attrFlags, message);
+            String errorMsg = "Attribute Flags Error for " + typeName + ": " +
+                attrFlags;
+            throw new BgpParseException(errorMsg);
+        }
+    }
+
+    /**
+     * Parses BGP UPDATE Attribute Type ORIGIN.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message to parse
+     * @return the parsed ORIGIN value
+     * @throws BgpParseException
+     */
+    private short parseAttributeTypeOrigin(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message)
+        throws BgpParseException {
+
+        // Check the Attribute Length
+        if (attrLen != BgpConstants.Update.Origin.LENGTH) {
+            // ERROR: Attribute Length Error
+            actionsBgpUpdateAttributeLengthError(
+                        ctx, attrTypeCode, attrLen, attrFlags, message);
+            String errorMsg = "Attribute Length Error";
+            throw new BgpParseException(errorMsg);
+        }
+
+        message.markReaderIndex();
+        short origin = message.readUnsignedByte();
+        switch (origin) {
+        case BgpConstants.Update.Origin.IGP:
+            // FALLTHROUGH
+        case BgpConstants.Update.Origin.EGP:
+            // FALLTHROUGH
+        case BgpConstants.Update.Origin.INCOMPLETE:
+            break;
+        default:
+            // ERROR: Invalid ORIGIN Attribute
+            message.resetReaderIndex();
+            actionsBgpUpdateInvalidOriginAttribute(
+                ctx, attrTypeCode, attrLen, attrFlags, message, origin);
+            String errorMsg = "Invalid ORIGIN Attribute: " + origin;
+            throw new BgpParseException(errorMsg);
+        }
+
+        return origin;
+    }
+
+    /**
+     * Parses BGP UPDATE Attribute AS Path.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message to parse
+     * @return the parsed AS Path
+     * @throws BgpParseException
+     */
+    private BgpRouteEntry.AsPath parseAttributeTypeAsPath(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message)
+        throws BgpParseException {
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+
+        //
+        // Parse the message
+        //
+        while (attrLen > 0) {
+            if (attrLen < 2) {
+                // ERROR: Malformed AS_PATH
+                actionsBgpUpdateMalformedAsPath(ctx);
+                String errorMsg = "Malformed AS Path";
+                throw new BgpParseException(errorMsg);
+            }
+            // Get the Path Segment Type and Length (in number of ASes)
+            short pathSegmentType = message.readUnsignedByte();
+            short pathSegmentLength = message.readUnsignedByte();
+            attrLen -= 2;
+
+            // Verify the Path Segment Type
+            switch (pathSegmentType) {
+            case BgpConstants.Update.AsPath.AS_SET:
+                // FALLTHROUGH
+            case BgpConstants.Update.AsPath.AS_SEQUENCE:
+                break;
+            default:
+                // ERROR: Invalid Path Segment Type
+                //
+                // NOTE: The BGP Spec (RFC 4271) doesn't contain Error Subcode
+                // for "Invalid Path Segment Type", hence we return
+                // the error as "Malformed AS_PATH".
+                //
+                actionsBgpUpdateMalformedAsPath(ctx);
+                String errorMsg =
+                    "Invalid AS Path Segment Type: " + pathSegmentType;
+                throw new BgpParseException(errorMsg);
+            }
+
+            // Parse the AS numbers
+            if (2 * pathSegmentLength > attrLen) {
+                // ERROR: Malformed AS_PATH
+                actionsBgpUpdateMalformedAsPath(ctx);
+                String errorMsg = "Malformed AS Path";
+                throw new BgpParseException(errorMsg);
+            }
+            attrLen -= (2 * pathSegmentLength);
+            ArrayList<Long> segmentAsNumbers = new ArrayList<>();
+            while (pathSegmentLength-- > 0) {
+                long asNumber = message.readUnsignedShort();
+                segmentAsNumbers.add(asNumber);
+            }
+
+            BgpRouteEntry.PathSegment pathSegment =
+                new BgpRouteEntry.PathSegment((byte) pathSegmentType,
+                                              segmentAsNumbers);
+            pathSegments.add(pathSegment);
+        }
+
+        return new BgpRouteEntry.AsPath(pathSegments);
+    }
+
+    /**
+     * Parses BGP UPDATE Attribute Type NEXT_HOP.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message to parse
+     * @return the parsed NEXT_HOP value
+     * @throws BgpParseException
+     */
+    private IpAddress parseAttributeTypeNextHop(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message)
+        throws BgpParseException {
+
+        // Check the Attribute Length
+        if (attrLen != BgpConstants.Update.NextHop.LENGTH) {
+            // ERROR: Attribute Length Error
+            actionsBgpUpdateAttributeLengthError(
+                        ctx, attrTypeCode, attrLen, attrFlags, message);
+            String errorMsg = "Attribute Length Error";
+            throw new BgpParseException(errorMsg);
+        }
+
+        message.markReaderIndex();
+        long address = message.readUnsignedInt();
+        IpAddress nextHopAddress = IpAddress.valueOf((int) address);
+        //
+        // Check whether the NEXT_HOP IP address is semantically correct.
+        // As per RFC 4271, Section 6.3:
+        //
+        //  a) It MUST NOT be the IP address of the receiving speaker
+        //  b) In the case of an EBGP ....
+        //
+        // Here we check only (a), because (b) doesn't apply for us: all our
+        // peers are iBGP.
+        //
+        if (nextHopAddress.equals(localIp4Address)) {
+            // ERROR: Invalid NEXT_HOP Attribute
+            message.resetReaderIndex();
+            actionsBgpUpdateInvalidNextHopAttribute(
+                        ctx, attrTypeCode, attrLen, attrFlags, message,
+                        nextHopAddress);
+            String errorMsg = "Invalid NEXT_HOP Attribute: " + nextHopAddress;
+            throw new BgpParseException(errorMsg);
+        }
+
+        return nextHopAddress;
+    }
+
+    /**
+     * Parses BGP UPDATE Attribute Type MULTI_EXIT_DISC.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message to parse
+     * @return the parsed MULTI_EXIT_DISC value
+     * @throws BgpParseException
+     */
+    private long parseAttributeTypeMultiExitDisc(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message)
+        throws BgpParseException {
+
+        // Check the Attribute Length
+        if (attrLen != BgpConstants.Update.MultiExitDisc.LENGTH) {
+            // ERROR: Attribute Length Error
+            actionsBgpUpdateAttributeLengthError(
+                        ctx, attrTypeCode, attrLen, attrFlags, message);
+            String errorMsg = "Attribute Length Error";
+            throw new BgpParseException(errorMsg);
+        }
+
+        long multiExitDisc = message.readUnsignedInt();
+        return multiExitDisc;
+    }
+
+    /**
+     * Parses BGP UPDATE Attribute Type LOCAL_PREF.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message to parse
+     * @return the parsed LOCAL_PREF value
+     * @throws BgpParseException
+     */
+    private long parseAttributeTypeLocalPref(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message)
+        throws BgpParseException {
+
+        // Check the Attribute Length
+        if (attrLen != BgpConstants.Update.LocalPref.LENGTH) {
+            // ERROR: Attribute Length Error
+            actionsBgpUpdateAttributeLengthError(
+                        ctx, attrTypeCode, attrLen, attrFlags, message);
+            String errorMsg = "Attribute Length Error";
+            throw new BgpParseException(errorMsg);
+        }
+
+        long localPref = message.readUnsignedInt();
+        return localPref;
+    }
+
+    /**
+     * Parses BGP UPDATE Attribute Type ATOMIC_AGGREGATE.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message to parse
+     * @throws BgpParseException
+     */
+    private void parseAttributeTypeAtomicAggregate(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message)
+        throws BgpParseException {
+
+        // Check the Attribute Length
+        if (attrLen != BgpConstants.Update.AtomicAggregate.LENGTH) {
+            // ERROR: Attribute Length Error
+            actionsBgpUpdateAttributeLengthError(
+                        ctx, attrTypeCode, attrLen, attrFlags, message);
+            String errorMsg = "Attribute Length Error";
+            throw new BgpParseException(errorMsg);
+        }
+
+        // Nothing to do: this attribute is primarily informational
+    }
+
+    /**
+     * Parses BGP UPDATE Attribute Type AGGREGATOR.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message to parse
+     * @return the parsed AGGREGATOR value: a tuple of <AS-Number, IP-Address>
+     * @throws BgpParseException
+     */
+    private Pair<Long, IpAddress> parseAttributeTypeAggregator(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message)
+        throws BgpParseException {
+
+        // Check the Attribute Length
+        if (attrLen != BgpConstants.Update.Aggregator.LENGTH) {
+            // ERROR: Attribute Length Error
+            actionsBgpUpdateAttributeLengthError(
+                        ctx, attrTypeCode, attrLen, attrFlags, message);
+            String errorMsg = "Attribute Length Error";
+            throw new BgpParseException(errorMsg);
+        }
+
+        // The AGGREGATOR AS number
+        long aggregatorAsNumber = message.readUnsignedShort();
+        // The AGGREGATOR IP address
+        long aggregatorAddress = message.readUnsignedInt();
+        IpAddress aggregatorIpAddress =
+                IpAddress.valueOf((int) aggregatorAddress);
+
+        Pair<Long, IpAddress> aggregator = Pair.of(aggregatorAsNumber,
+                                                    aggregatorIpAddress);
+        return aggregator;
+    }
+
+    /**
+     * Parses a message that contains encoded IPv4 network prefixes.
+     * <p>
+     * The IPv4 prefixes are encoded in the form:
+     * <Length, Prefix> where Length is the length in bits of the IPv4 prefix,
+     * and Prefix is the IPv4 prefix (padded with trailing bits to the end
+     * of an octet).
+     *
+     * @param totalLength the total length of the data to parse
+     * @param message the message with data to parse
+     * @return a collection of parsed IPv4 network prefixes
+     * @throws BgpParseException
+     */
+    private Collection<IpPrefix> parsePackedPrefixes(int totalLength,
+                                                      ChannelBuffer message)
+        throws BgpParseException {
+        Collection<IpPrefix> result = new ArrayList<>();
+
+        if (totalLength == 0) {
+            return result;
+        }
+
+        // Parse the data
+        int dataEnd = message.readerIndex() + totalLength;
+        while (message.readerIndex() < dataEnd) {
+            int prefixBitlen = message.readUnsignedByte();
+            int prefixBytelen = (prefixBitlen + 7) / 8;     // Round-up
+            if (message.readerIndex() + prefixBytelen > dataEnd) {
+                String errorMsg = "Malformed Network Prefixes";
+                throw new BgpParseException(errorMsg);
+            }
+
+            long address = 0;
+            long extraShift = (4 - prefixBytelen) * 8;
+            while (prefixBytelen > 0) {
+                address <<= 8;
+                address |= message.readUnsignedByte();
+                prefixBytelen--;
+            }
+            address <<= extraShift;
+            IpPrefix prefix =
+                IpPrefix.valueOf(IpAddress.valueOf((int) address).toInt(),
+                              (short) prefixBitlen);
+            result.add(prefix);
+        }
+
+        return result;
+    }
+
+    /**
+     * Applies the appropriate actions after detecting BGP UPDATE
+     * Invalid Network Field Error: send NOTIFICATION and close the channel.
+     *
+     * @param ctx the Channel Handler Context
+     */
+    private void actionsBgpUpdateInvalidNetworkField(
+                        ChannelHandlerContext ctx) {
+        log.debug("BGP RX UPDATE Error from {}: Invalid Network Field",
+                  remoteAddress);
+
+        //
+        // ERROR: Invalid Network Field
+        //
+        // Send NOTIFICATION and close the connection
+        int errorCode = UpdateMessageError.ERROR_CODE;
+        int errorSubcode = UpdateMessageError.INVALID_NETWORK_FIELD;
+        ChannelBuffer txMessage =
+            prepareBgpNotification(errorCode, errorSubcode, null);
+        ctx.getChannel().write(txMessage);
+        closeChannel(ctx);
+    }
+
+    /**
+     * Applies the appropriate actions after detecting BGP UPDATE
+     * Malformed Attribute List Error: send NOTIFICATION and close the channel.
+     *
+     * @param ctx the Channel Handler Context
+     */
+    private void actionsBgpUpdateMalformedAttributeList(
+                        ChannelHandlerContext ctx) {
+        log.debug("BGP RX UPDATE Error from {}: Malformed Attribute List",
+                  remoteAddress);
+
+        //
+        // ERROR: Malformed Attribute List
+        //
+        // Send NOTIFICATION and close the connection
+        int errorCode = UpdateMessageError.ERROR_CODE;
+        int errorSubcode = UpdateMessageError.MALFORMED_ATTRIBUTE_LIST;
+        ChannelBuffer txMessage =
+            prepareBgpNotification(errorCode, errorSubcode, null);
+        ctx.getChannel().write(txMessage);
+        closeChannel(ctx);
+    }
+
+    /**
+     * Applies the appropriate actions after detecting BGP UPDATE
+     * Missing Well-known Attribute Error: send NOTIFICATION and close the
+     * channel.
+     *
+     * @param ctx the Channel Handler Context
+     * @param missingAttrTypeCode the missing attribute type code
+     */
+    private void actionsBgpUpdateMissingWellKnownAttribute(
+                        ChannelHandlerContext ctx,
+                        int missingAttrTypeCode) {
+        log.debug("BGP RX UPDATE Error from {}: Missing Well-known Attribute: {}",
+                  remoteAddress, missingAttrTypeCode);
+
+        //
+        // ERROR: Missing Well-known Attribute
+        //
+        // Send NOTIFICATION and close the connection
+        int errorCode = UpdateMessageError.ERROR_CODE;
+        int errorSubcode = UpdateMessageError.MISSING_WELL_KNOWN_ATTRIBUTE;
+        ChannelBuffer data = ChannelBuffers.buffer(1);
+        data.writeByte(missingAttrTypeCode);
+        ChannelBuffer txMessage =
+            prepareBgpNotification(errorCode, errorSubcode, data);
+        ctx.getChannel().write(txMessage);
+        closeChannel(ctx);
+    }
+
+    /**
+     * Applies the appropriate actions after detecting BGP UPDATE
+     * Invalid ORIGIN Attribute Error: send NOTIFICATION and close the channel.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message with the data
+     * @param origin the ORIGIN attribute value
+     */
+    private void actionsBgpUpdateInvalidOriginAttribute(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message,
+                        short origin) {
+        log.debug("BGP RX UPDATE Error from {}: Invalid ORIGIN Attribute",
+                  remoteAddress);
+
+        //
+        // ERROR: Invalid ORIGIN Attribute
+        //
+        // Send NOTIFICATION and close the connection
+        int errorCode = UpdateMessageError.ERROR_CODE;
+        int errorSubcode = UpdateMessageError.INVALID_ORIGIN_ATTRIBUTE;
+        ChannelBuffer data =
+            prepareBgpUpdateNotificationDataPayload(attrTypeCode, attrLen,
+                                                    attrFlags, message);
+        ChannelBuffer txMessage =
+            prepareBgpNotification(errorCode, errorSubcode, data);
+        ctx.getChannel().write(txMessage);
+        closeChannel(ctx);
+    }
+
+    /**
+     * Applies the appropriate actions after detecting BGP UPDATE
+     * Attribute Flags Error: send NOTIFICATION and close the channel.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message with the data
+     */
+    private void actionsBgpUpdateAttributeFlagsError(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message) {
+        log.debug("BGP RX UPDATE Error from {}: Attribute Flags Error",
+                  remoteAddress);
+
+        //
+        // ERROR: Attribute Flags Error
+        //
+        // Send NOTIFICATION and close the connection
+        int errorCode = UpdateMessageError.ERROR_CODE;
+        int errorSubcode = UpdateMessageError.ATTRIBUTE_FLAGS_ERROR;
+        ChannelBuffer data =
+            prepareBgpUpdateNotificationDataPayload(attrTypeCode, attrLen,
+                                                    attrFlags, message);
+        ChannelBuffer txMessage =
+            prepareBgpNotification(errorCode, errorSubcode, data);
+        ctx.getChannel().write(txMessage);
+        closeChannel(ctx);
+    }
+
+    /**
+     * Applies the appropriate actions after detecting BGP UPDATE
+     * Invalid NEXT_HOP Attribute Error: send NOTIFICATION and close the
+     * channel.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message with the data
+     * @param nextHop the NEXT_HOP attribute value
+     */
+    private void actionsBgpUpdateInvalidNextHopAttribute(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message,
+                        IpAddress nextHop) {
+        log.debug("BGP RX UPDATE Error from {}: Invalid NEXT_HOP Attribute {}",
+                  remoteAddress, nextHop);
+
+        //
+        // ERROR: Invalid ORIGIN Attribute
+        //
+        // Send NOTIFICATION and close the connection
+        int errorCode = UpdateMessageError.ERROR_CODE;
+        int errorSubcode = UpdateMessageError.INVALID_NEXT_HOP_ATTRIBUTE;
+        ChannelBuffer data =
+            prepareBgpUpdateNotificationDataPayload(attrTypeCode, attrLen,
+                                                    attrFlags, message);
+        ChannelBuffer txMessage =
+            prepareBgpNotification(errorCode, errorSubcode, data);
+        ctx.getChannel().write(txMessage);
+        closeChannel(ctx);
+    }
+
+    /**
+     * Applies the appropriate actions after detecting BGP UPDATE
+     * Unrecognized Well-known Attribute Error: send NOTIFICATION and close
+     * the channel.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message with the data
+     */
+    private void actionsBgpUpdateUnrecognizedWellKnownAttribute(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message) {
+        log.debug("BGP RX UPDATE Error from {}: " +
+                  "Unrecognized Well-known Attribute Error: {}",
+                  remoteAddress, attrTypeCode);
+
+        //
+        // ERROR: Unrecognized Well-known Attribute
+        //
+        // Send NOTIFICATION and close the connection
+        int errorCode = UpdateMessageError.ERROR_CODE;
+        int errorSubcode =
+            UpdateMessageError.UNRECOGNIZED_WELL_KNOWN_ATTRIBUTE;
+        ChannelBuffer data =
+            prepareBgpUpdateNotificationDataPayload(attrTypeCode, attrLen,
+                                                    attrFlags, message);
+        ChannelBuffer txMessage =
+            prepareBgpNotification(errorCode, errorSubcode, data);
+        ctx.getChannel().write(txMessage);
+        closeChannel(ctx);
+    }
+
+    /**
+     * Applies the appropriate actions after detecting BGP UPDATE
+     * Attribute Length Error: send NOTIFICATION and close the channel.
+     *
+     * @param ctx the Channel Handler Context
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message with the data
+     */
+    private void actionsBgpUpdateAttributeLengthError(
+                        ChannelHandlerContext ctx,
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message) {
+        log.debug("BGP RX UPDATE Error from {}: Attribute Length Error",
+                  remoteAddress);
+
+        //
+        // ERROR: Attribute Length Error
+        //
+        // Send NOTIFICATION and close the connection
+        int errorCode = UpdateMessageError.ERROR_CODE;
+        int errorSubcode = UpdateMessageError.ATTRIBUTE_LENGTH_ERROR;
+        ChannelBuffer data =
+            prepareBgpUpdateNotificationDataPayload(attrTypeCode, attrLen,
+                                                    attrFlags, message);
+        ChannelBuffer txMessage =
+            prepareBgpNotification(errorCode, errorSubcode, data);
+        ctx.getChannel().write(txMessage);
+        closeChannel(ctx);
+    }
+
+    /**
+     * Applies the appropriate actions after detecting BGP UPDATE
+     * Malformed AS_PATH Error: send NOTIFICATION and close the channel.
+     *
+     * @param ctx the Channel Handler Context
+     */
+    private void actionsBgpUpdateMalformedAsPath(
+                        ChannelHandlerContext ctx) {
+        log.debug("BGP RX UPDATE Error from {}: Malformed AS Path",
+                  remoteAddress);
+
+        //
+        // ERROR: Malformed AS_PATH
+        //
+        // Send NOTIFICATION and close the connection
+        int errorCode = UpdateMessageError.ERROR_CODE;
+        int errorSubcode = UpdateMessageError.MALFORMED_AS_PATH;
+        ChannelBuffer txMessage =
+            prepareBgpNotification(errorCode, errorSubcode, null);
+        ctx.getChannel().write(txMessage);
+        closeChannel(ctx);
+    }
+
+    /**
+     * Processes BGP NOTIFICATION message.
+     *
+     * @param ctx the Channel Handler Context
+     * @param message the message to process
+     */
+    void processBgpNotification(ChannelHandlerContext ctx,
+                                ChannelBuffer message) {
+        int minLength =
+            BgpConstants.BGP_NOTIFICATION_MIN_LENGTH - BgpConstants.BGP_HEADER_LENGTH;
+        if (message.readableBytes() < minLength) {
+            log.debug("BGP RX NOTIFICATION Error from {}: " +
+                      "Message length {} too short. Must be at least {}",
+                      remoteAddress, message.readableBytes(), minLength);
+            //
+            // ERROR: Bad Message Length
+            //
+            // NOTE: We do NOT send NOTIFICATION in response to a notification
+            return;
+        }
+
+        //
+        // Parse the NOTIFICATION message
+        //
+        int errorCode = message.readUnsignedByte();
+        int errorSubcode = message.readUnsignedByte();
+        int dataLength = message.readableBytes();
+
+        log.debug("BGP RX NOTIFICATION message from {}: Error Code {} " +
+                  "Error Subcode {} Data Length {}",
+                  remoteAddress, errorCode, errorSubcode, dataLength);
+
+        //
+        // NOTE: If the peer sent a NOTIFICATION, we leave it to the peer to
+        // close the connection.
+        //
+
+        // Start the Session Timeout timer
+        restartSessionTimeoutTimer(ctx);
+    }
+
+    /**
+     * Processes BGP KEEPALIVE message.
+     *
+     * @param ctx the Channel Handler Context
+     * @param message the message to process
+     */
+    void processBgpKeepalive(ChannelHandlerContext ctx,
+                             ChannelBuffer message) {
+        if (message.readableBytes() + BgpConstants.BGP_HEADER_LENGTH !=
+            BgpConstants.BGP_KEEPALIVE_EXPECTED_LENGTH) {
+            log.debug("BGP RX KEEPALIVE Error from {}: " +
+                      "Invalid total message length {}. Expected {}",
+                      remoteAddress,
+                      message.readableBytes() + BgpConstants.BGP_HEADER_LENGTH,
+                      BgpConstants.BGP_KEEPALIVE_EXPECTED_LENGTH);
+            //
+            // ERROR: Bad Message Length
+            //
+            // Send NOTIFICATION and close the connection
+            ChannelBuffer txMessage = prepareBgpNotificationBadMessageLength(
+                message.readableBytes() + BgpConstants.BGP_HEADER_LENGTH);
+            ctx.getChannel().write(txMessage);
+            closeChannel(ctx);
+            return;
+        }
+
+        //
+        // Parse the KEEPALIVE message: nothing to do
+        //
+        log.debug("BGP RX KEEPALIVE message from {}", remoteAddress);
+
+        // Start the Session Timeout timer
+        restartSessionTimeoutTimer(ctx);
+    }
+
+    /**
+     * Prepares BGP OPEN message.
+     *
+     * @return the message to transmit (BGP header included)
+     */
+    private ChannelBuffer prepareBgpOpen() {
+        ChannelBuffer message =
+            ChannelBuffers.buffer(BgpConstants.BGP_MESSAGE_MAX_LENGTH);
+
+        //
+        // Prepare the OPEN message payload
+        //
+        message.writeByte(localBgpVersion);
+        message.writeShort((int) localAs);
+        message.writeShort((int) localHoldtime);
+        message.writeInt(bgpSessionManager.getMyBgpId().toInt());
+        message.writeByte(0);               // No Optional Parameters
+        return prepareBgpMessage(BgpConstants.BGP_TYPE_OPEN, message);
+    }
+
+    /**
+     * Prepares BGP KEEPALIVE message.
+     *
+     * @return the message to transmit (BGP header included)
+     */
+    private ChannelBuffer prepareBgpKeepalive() {
+        ChannelBuffer message =
+            ChannelBuffers.buffer(BgpConstants.BGP_MESSAGE_MAX_LENGTH);
+
+        //
+        // Prepare the KEEPALIVE message payload: nothing to do
+        //
+        return prepareBgpMessage(BgpConstants.BGP_TYPE_KEEPALIVE, message);
+    }
+
+    /**
+     * Prepares BGP NOTIFICATION message.
+     *
+     * @param errorCode the BGP NOTIFICATION Error Code
+     * @param errorSubcode the BGP NOTIFICATION Error Subcode if applicable,
+     * otherwise BgpConstants.Notifications.ERROR_SUBCODE_UNSPECIFIC
+     * @param payload the BGP NOTIFICATION Data if applicable, otherwise null
+     * @return the message to transmit (BGP header included)
+     */
+    ChannelBuffer prepareBgpNotification(int errorCode, int errorSubcode,
+                                         ChannelBuffer data) {
+        ChannelBuffer message =
+            ChannelBuffers.buffer(BgpConstants.BGP_MESSAGE_MAX_LENGTH);
+
+        //
+        // Prepare the NOTIFICATION message payload
+        //
+        message.writeByte(errorCode);
+        message.writeByte(errorSubcode);
+        if (data != null) {
+            message.writeBytes(data);
+        }
+        return prepareBgpMessage(BgpConstants.BGP_TYPE_NOTIFICATION, message);
+    }
+
+    /**
+     * Prepares BGP NOTIFICATION message: Bad Message Length.
+     *
+     * @param length the erroneous Length field
+     * @return the message to transmit (BGP header included)
+     */
+    ChannelBuffer prepareBgpNotificationBadMessageLength(int length) {
+        int errorCode = MessageHeaderError.ERROR_CODE;
+        int errorSubcode = MessageHeaderError.BAD_MESSAGE_LENGTH;
+        ChannelBuffer data = ChannelBuffers.buffer(2);
+        data.writeShort(length);
+
+        return prepareBgpNotification(errorCode, errorSubcode, data);
+    }
+
+    /**
+     * Prepares BGP UPDATE Notification data payload.
+     *
+     * @param attrTypeCode the attribute type code
+     * @param attrLen the attribute length (in octets)
+     * @param attrFlags the attribute flags
+     * @param message the message with the data
+     * @return the buffer with the data payload for the BGP UPDATE Notification
+     */
+    private ChannelBuffer prepareBgpUpdateNotificationDataPayload(
+                        int attrTypeCode,
+                        int attrLen,
+                        int attrFlags,
+                        ChannelBuffer message) {
+        // Compute the attribute length field octets
+        boolean extendedLengthBit = ((0x10 & attrFlags) != 0);
+        int attrLenOctets = 1;
+        if (extendedLengthBit) {
+            attrLenOctets = 2;
+        }
+        ChannelBuffer data =
+            ChannelBuffers.buffer(attrLen + attrLenOctets + 1);
+        data.writeByte(attrTypeCode);
+        if (extendedLengthBit) {
+            data.writeShort(attrLen);
+        } else {
+            data.writeByte(attrLen);
+        }
+        data.writeBytes(message, attrLen);
+        return data;
+    }
+
+    /**
+     * Prepares BGP message.
+     *
+     * @param type the BGP message type
+     * @param payload the message payload to transmit (BGP header excluded)
+     * @return the message to transmit (BGP header included)
+     */
+    private ChannelBuffer prepareBgpMessage(int type, ChannelBuffer payload) {
+        ChannelBuffer message =
+            ChannelBuffers.buffer(BgpConstants.BGP_HEADER_LENGTH +
+                                  payload.readableBytes());
+
+        // Write the marker
+        for (int i = 0; i < BgpConstants.BGP_HEADER_MARKER_LENGTH; i++) {
+            message.writeByte(0xff);
+        }
+
+        // Write the rest of the BGP header
+        message.writeShort(BgpConstants.BGP_HEADER_LENGTH +
+                           payload.readableBytes());
+        message.writeByte(type);
+
+        // Write the payload
+        message.writeBytes(payload);
+        return message;
+    }
+
+    /**
+     * Restarts the BGP KeepaliveTimer.
+     */
+    private void restartKeepaliveTimer(ChannelHandlerContext ctx) {
+        if (localKeepaliveInterval == 0) {
+            return;                 // Nothing to do
+        }
+        keepaliveTimeout = timer.newTimeout(new TransmitKeepaliveTask(ctx),
+                                            localKeepaliveInterval,
+                                            TimeUnit.SECONDS);
+    }
+
+    /**
+     * Task class for transmitting KEEPALIVE messages.
+     */
+    private final class TransmitKeepaliveTask implements TimerTask {
+        private final ChannelHandlerContext ctx;
+
+        /**
+         * Constructor for given Channel Handler Context.
+         *
+         * @param ctx the Channel Handler Context to use
+         */
+        TransmitKeepaliveTask(ChannelHandlerContext ctx) {
+            this.ctx = ctx;
+        }
+
+        @Override
+        public void run(Timeout timeout) throws Exception {
+            if (timeout.isCancelled()) {
+                return;
+            }
+            if (!ctx.getChannel().isOpen()) {
+                return;
+            }
+
+            // Transmit the KEEPALIVE
+            ChannelBuffer txMessage = prepareBgpKeepalive();
+            ctx.getChannel().write(txMessage);
+
+            // Restart the KEEPALIVE timer
+            restartKeepaliveTimer(ctx);
+        }
+    }
+
+    /**
+     * Restarts the BGP Session Timeout Timer.
+     */
+    private void restartSessionTimeoutTimer(ChannelHandlerContext ctx) {
+        if (remoteHoldtime == 0) {
+            return;                 // Nothing to do
+        }
+        if (sessionTimeout != null) {
+            sessionTimeout.cancel();
+        }
+        sessionTimeout = timer.newTimeout(new SessionTimeoutTask(ctx),
+                                          remoteHoldtime,
+                                          TimeUnit.SECONDS);
+    }
+
+    /**
+     * Task class for BGP Session timeout.
+     */
+    private final class SessionTimeoutTask implements TimerTask {
+        private final ChannelHandlerContext ctx;
+
+        /**
+         * Constructor for given Channel Handler Context.
+         *
+         * @param ctx the Channel Handler Context to use
+         */
+        SessionTimeoutTask(ChannelHandlerContext ctx) {
+            this.ctx = ctx;
+        }
+
+        @Override
+        public void run(Timeout timeout) throws Exception {
+            if (timeout.isCancelled()) {
+                return;
+            }
+            if (!ctx.getChannel().isOpen()) {
+                return;
+            }
+
+            log.debug("BGP Session Timeout: peer {}", remoteAddress);
+            //
+            // ERROR: Invalid Optional Parameter Length field: Unspecific
+            //
+            // Send NOTIFICATION and close the connection
+            int errorCode = HoldTimerExpired.ERROR_CODE;
+            int errorSubcode = Notifications.ERROR_SUBCODE_UNSPECIFIC;
+            ChannelBuffer txMessage =
+                prepareBgpNotification(errorCode, errorSubcode, null);
+            ctx.getChannel().write(txMessage);
+            closeChannel(ctx);
+        }
+    }
+
+    /**
+     * An exception indicating a parsing error of the BGP message.
+     */
+    private static class BgpParseException extends Exception {
+        /**
+         * Default constructor.
+         */
+        public BgpParseException() {
+            super();
+        }
+
+        /**
+         * Constructor for a specific exception details message.
+         *
+         * @param message the message with the exception details
+         */
+         public BgpParseException(String message) {
+             super(message);
+         }
+    }
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpSessionManager.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpSessionManager.java
new file mode 100644
index 0000000..097c002
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpSessionManager.java
@@ -0,0 +1,355 @@
+package org.onlab.onos.sdnip.bgp;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+
+import org.jboss.netty.bootstrap.ServerBootstrap;
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelException;
+import org.jboss.netty.channel.ChannelFactory;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ChannelPipelineFactory;
+import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
+import org.onlab.onos.sdnip.RouteListener;
+import org.onlab.onos.sdnip.RouteUpdate;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * BGP Session Manager class.
+ */
+public class BgpSessionManager {
+    private static final Logger log =
+        LoggerFactory.getLogger(BgpSessionManager.class);
+    private Channel serverChannel;     // Listener for incoming BGP connections
+    private ConcurrentMap<SocketAddress, BgpSession> bgpSessions =
+        new ConcurrentHashMap<>();
+    private IpAddress myBgpId;         // Same BGP ID for all peers
+
+    private BgpRouteSelector bgpRouteSelector = new BgpRouteSelector();
+    private ConcurrentMap<IpPrefix, BgpRouteEntry> bgpRoutes =
+        new ConcurrentHashMap<>();
+
+    private final RouteListener routeListener;
+
+    /**
+     * Constructor for given route listener.
+     *
+     * @param routeListener the route listener to use
+     */
+    public BgpSessionManager(RouteListener routeListener) {
+        this.routeListener = checkNotNull(routeListener);
+    }
+
+    /**
+     * Gets the BGP sessions.
+     *
+     * @return the BGP sessions
+     */
+    public Collection<BgpSession> getBgpSessions() {
+        return bgpSessions.values();
+    }
+
+    /**
+     * Gets the BGP routes.
+     *
+     * @return the BGP routes
+     */
+    public Collection<BgpRouteEntry> getBgpRoutes() {
+        return bgpRoutes.values();
+    }
+
+    /**
+     * Processes the connection from a BGP peer.
+     *
+     * @param bgpSession the BGP session for the peer
+     * @return true if the connection can be established, otherwise false
+     */
+    boolean peerConnected(BgpSession bgpSession) {
+
+        // Test whether there is already a session from the same remote
+        if (bgpSessions.get(bgpSession.getRemoteAddress()) != null) {
+            return false;               // Duplicate BGP session
+        }
+        bgpSessions.put(bgpSession.getRemoteAddress(), bgpSession);
+
+        //
+        // If the first connection, set my BGP ID to the local address
+        // of the socket.
+        //
+        if (bgpSession.getLocalAddress() instanceof InetSocketAddress) {
+            InetAddress inetAddr =
+                ((InetSocketAddress) bgpSession.getLocalAddress()).getAddress();
+            IpAddress ip4Address = IpAddress.valueOf(inetAddr.getAddress());
+            updateMyBgpId(ip4Address);
+        }
+        return true;
+    }
+
+    /**
+     * Processes the disconnection from a BGP peer.
+     *
+     * @param bgpSession the BGP session for the peer
+     */
+    void peerDisconnected(BgpSession bgpSession) {
+        bgpSessions.remove(bgpSession.getRemoteAddress());
+    }
+
+    /**
+     * Conditionally updates the local BGP ID if it wasn't set already.
+     * <p/>
+     * NOTE: A BGP instance should use same BGP ID across all BGP sessions.
+     *
+     * @param ip4Address the IPv4 address to use as BGP ID
+     */
+    private synchronized void updateMyBgpId(IpAddress ip4Address) {
+        if (myBgpId == null) {
+            myBgpId = ip4Address;
+            log.debug("BGP: My BGP ID is {}", myBgpId);
+        }
+    }
+
+    /**
+     * Gets the local BGP Identifier as an IPv4 address.
+     *
+     * @return the local BGP Identifier as an IPv4 address
+     */
+    IpAddress getMyBgpId() {
+        return myBgpId;
+    }
+
+    /**
+     * Gets the BGP Route Selector.
+     *
+     * @return the BGP Route Selector
+     */
+    BgpRouteSelector getBgpRouteSelector() {
+        return bgpRouteSelector;
+    }
+
+    /**
+     * Starts up BGP Session Manager operation.
+     *
+     * @param listenPortNumber the port number to listen on. By default
+     * it should be BgpConstants.BGP_PORT (179)
+     */
+    public void startUp(int listenPortNumber) {
+        log.debug("BGP Session Manager startUp()");
+
+        ChannelFactory channelFactory =
+            new NioServerSocketChannelFactory(Executors.newCachedThreadPool(),
+                                              Executors.newCachedThreadPool());
+        ChannelPipelineFactory pipelineFactory = new ChannelPipelineFactory() {
+                @Override
+                public ChannelPipeline getPipeline() throws Exception {
+                    // Allocate a new session per connection
+                    BgpSession bgpSessionHandler =
+                        new BgpSession(BgpSessionManager.this);
+                    BgpFrameDecoder bgpFrameDecoder =
+                        new BgpFrameDecoder(bgpSessionHandler);
+
+                    // Setup the processing pipeline
+                    ChannelPipeline pipeline = Channels.pipeline();
+                    pipeline.addLast("BgpFrameDecoder", bgpFrameDecoder);
+                    pipeline.addLast("BgpSession", bgpSessionHandler);
+                    return pipeline;
+                }
+            };
+        InetSocketAddress listenAddress =
+            new InetSocketAddress(listenPortNumber);
+
+        ServerBootstrap serverBootstrap = new ServerBootstrap(channelFactory);
+        // serverBootstrap.setOptions("reuseAddr", true);
+        serverBootstrap.setOption("child.keepAlive", true);
+        serverBootstrap.setOption("child.tcpNoDelay", true);
+        serverBootstrap.setPipelineFactory(pipelineFactory);
+        try {
+            serverChannel = serverBootstrap.bind(listenAddress);
+        } catch (ChannelException e) {
+            log.debug("Exception binding to BGP port {}: ",
+                      listenAddress.getPort(), e);
+        }
+    }
+
+    /**
+     * Shuts down the BGP Session Manager operation.
+     */
+    public void shutDown() {
+        // TODO: Complete the implementation: remove routes, etc.
+        if (serverChannel != null) {
+            serverChannel.close();
+        }
+    }
+
+    /**
+     * Class to receive and process the BGP routes from each BGP Session/Peer.
+     */
+    class BgpRouteSelector {
+        /**
+         * Processes route entry updates: added/updated and deleted route
+         * entries.
+         *
+         * @param bgpSession the BGP session the route entry updates were
+         * received on
+         * @param addedBgpRouteEntries the added/updated route entries to
+         * process
+         * @param deletedBgpRouteEntries the deleted route entries to process
+         */
+        synchronized void routeUpdates(BgpSession bgpSession,
+                        Collection<BgpRouteEntry> addedBgpRouteEntries,
+                        Collection<BgpRouteEntry> deletedBgpRouteEntries) {
+            //
+            // TODO: Merge the updates from different BGP Peers,
+            // by choosing the best route.
+            //
+
+            // Process the deleted route entries
+            for (BgpRouteEntry bgpRouteEntry : deletedBgpRouteEntries) {
+                processDeletedRoute(bgpSession, bgpRouteEntry);
+            }
+
+            // Process the added/updated route entries
+            for (BgpRouteEntry bgpRouteEntry : addedBgpRouteEntries) {
+                processAddedRoute(bgpSession, bgpRouteEntry);
+            }
+        }
+
+        /**
+         * Processes an added/updated route entry.
+         *
+         * @param bgpSession the BGP session the route entry update was
+         * received on
+         * @param bgpRouteEntry the added/updated route entry
+         */
+        private void processAddedRoute(BgpSession bgpSession,
+                                       BgpRouteEntry bgpRouteEntry) {
+            RouteUpdate routeUpdate;
+            BgpRouteEntry bestBgpRouteEntry =
+                bgpRoutes.get(bgpRouteEntry.prefix());
+
+            //
+            // Install the new route entry if it is better than the
+            // current best route.
+            //
+            if ((bestBgpRouteEntry == null) ||
+                bgpRouteEntry.isBetterThan(bestBgpRouteEntry)) {
+                bgpRoutes.put(bgpRouteEntry.prefix(), bgpRouteEntry);
+                routeUpdate =
+                    new RouteUpdate(RouteUpdate.Type.UPDATE, bgpRouteEntry);
+                // Forward the result route updates to the Route Listener
+                routeListener.update(routeUpdate);
+                return;
+            }
+
+            //
+            // If the route entry arrived on the same BGP Session as
+            // the current best route, then elect the next best route
+            // and install it.
+            //
+            if (bestBgpRouteEntry.getBgpSession() !=
+                bgpRouteEntry.getBgpSession()) {
+                return;
+            }
+
+            // Find the next best route
+            bestBgpRouteEntry = findBestBgpRoute(bgpRouteEntry.prefix());
+            if (bestBgpRouteEntry == null) {
+                //
+                // TODO: Shouldn't happen. Install the new route as a
+                // pre-caution.
+                //
+                log.debug("BGP next best route for prefix {} is missing. " +
+                          "Adding the route that is currently processed.",
+                          bgpRouteEntry.prefix());
+                bestBgpRouteEntry = bgpRouteEntry;
+            }
+            // Install the next best route
+            bgpRoutes.put(bestBgpRouteEntry.prefix(), bestBgpRouteEntry);
+            routeUpdate = new RouteUpdate(RouteUpdate.Type.UPDATE,
+                                          bestBgpRouteEntry);
+            // Forward the result route updates to the Route Listener
+            routeListener.update(routeUpdate);
+        }
+
+        /**
+         * Processes a deleted route entry.
+         *
+         * @param bgpSession the BGP session the route entry update was
+         * received on
+         * @param bgpRouteEntry the deleted route entry
+         */
+        private void processDeletedRoute(BgpSession bgpSession,
+                                         BgpRouteEntry bgpRouteEntry) {
+            RouteUpdate routeUpdate;
+            BgpRouteEntry bestBgpRouteEntry =
+                bgpRoutes.get(bgpRouteEntry.prefix());
+
+            //
+            // Remove the route entry only if it was the best one.
+            // Install the the next best route if it exists.
+            //
+            // NOTE: We intentionally use "==" instead of method equals(),
+            // because we need to check whether this is same object.
+            //
+            if (bgpRouteEntry != bestBgpRouteEntry) {
+                return;         // Nothing to do
+            }
+
+            //
+            // Find the next best route
+            //
+            bestBgpRouteEntry = findBestBgpRoute(bgpRouteEntry.prefix());
+            if (bestBgpRouteEntry != null) {
+                // Install the next best route
+                bgpRoutes.put(bestBgpRouteEntry.prefix(),
+                              bestBgpRouteEntry);
+                routeUpdate = new RouteUpdate(RouteUpdate.Type.UPDATE,
+                                              bestBgpRouteEntry);
+                // Forward the result route updates to the Route Listener
+                routeListener.update(routeUpdate);
+                return;
+            }
+
+            //
+            // No route found. Remove the route entry
+            //
+            bgpRoutes.remove(bgpRouteEntry.prefix());
+            routeUpdate = new RouteUpdate(RouteUpdate.Type.DELETE,
+                                          bgpRouteEntry);
+            // Forward the result route updates to the Route Listener
+            routeListener.update(routeUpdate);
+        }
+
+        /**
+         * Finds the best route entry among all BGP Sessions.
+         *
+         * @param prefix the prefix of the route
+         * @return the best route if found, otherwise null
+         */
+        private BgpRouteEntry findBestBgpRoute(IpPrefix prefix) {
+            BgpRouteEntry bestRoute = null;
+
+            // Iterate across all BGP Sessions and select the best route
+            for (BgpSession bgpSession : bgpSessions.values()) {
+                BgpRouteEntry route = bgpSession.findBgpRouteEntry(prefix);
+                if (route == null) {
+                    continue;
+                }
+                if ((bestRoute == null) || route.isBetterThan(bestRoute)) {
+                    bestRoute = route;
+                }
+            }
+            return bestRoute;
+        }
+    }
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/package-info.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/package-info.java
new file mode 100644
index 0000000..e39d7e0
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Implementation of the BGP protocol.
+ */
+package org.onlab.onos.sdnip.bgp;
\ No newline at end of file
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouteEntryTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouteEntryTest.java
new file mode 100644
index 0000000..45371f7
--- /dev/null
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouteEntryTest.java
@@ -0,0 +1,143 @@
+package org.onlab.onos.sdnip;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+
+/**
+ * Unit tests for the RouteEntry class.
+ */
+public class RouteEntryTest {
+    /**
+     * Tests valid class constructor.
+     */
+    @Test
+    public void testConstructor() {
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+
+        RouteEntry routeEntry = new RouteEntry(prefix, nextHop);
+        assertThat(routeEntry.toString(),
+                   is("RouteEntry{prefix=1.2.3.0/24, nextHop=5.6.7.8}"));
+    }
+
+    /**
+     * Tests invalid class constructor for null IPv4 prefix.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullPrefix() {
+        IpPrefix prefix = null;
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+
+        new RouteEntry(prefix, nextHop);
+    }
+
+    /**
+     * Tests invalid class constructor for null IPv4 next-hop.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullNextHop() {
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = null;
+
+        new RouteEntry(prefix, nextHop);
+    }
+
+    /**
+     * Tests getting the fields of a route entry.
+     */
+    @Test
+    public void testGetFields() {
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+
+        RouteEntry routeEntry = new RouteEntry(prefix, nextHop);
+        assertThat(routeEntry.prefix(), is(prefix));
+        assertThat(routeEntry.nextHop(), is(nextHop));
+    }
+
+    /**
+     * Tests creating a binary string from IPv4 prefix.
+     */
+    @Test
+    public void testCreateBinaryString() {
+        IpPrefix prefix;
+
+        prefix = IpPrefix.valueOf("0.0.0.0/0");
+        assertThat(RouteEntry.createBinaryString(prefix), is(""));
+
+        prefix = IpPrefix.valueOf("192.168.166.0/22");
+        assertThat(RouteEntry.createBinaryString(prefix),
+                   is("1100000010101000101001"));
+
+        prefix = IpPrefix.valueOf("192.168.166.0/23");
+        assertThat(RouteEntry.createBinaryString(prefix),
+                   is("11000000101010001010011"));
+
+        prefix = IpPrefix.valueOf("192.168.166.0/24");
+        assertThat(RouteEntry.createBinaryString(prefix),
+                   is("110000001010100010100110"));
+
+        prefix = IpPrefix.valueOf("130.162.10.1/25");
+        assertThat(RouteEntry.createBinaryString(prefix),
+                   is("1000001010100010000010100"));
+
+        prefix = IpPrefix.valueOf("255.255.255.255/32");
+        assertThat(RouteEntry.createBinaryString(prefix),
+                   is("11111111111111111111111111111111"));
+    }
+
+    /**
+     * Tests equality of {@link RouteEntry}.
+     */
+    @Test
+    public void testEquality() {
+        IpPrefix prefix1 = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop1 = IpAddress.valueOf("5.6.7.8");
+        RouteEntry routeEntry1 = new RouteEntry(prefix1, nextHop1);
+
+        IpPrefix prefix2 = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop2 = IpAddress.valueOf("5.6.7.8");
+        RouteEntry routeEntry2 = new RouteEntry(prefix2, nextHop2);
+
+        assertThat(routeEntry1, is(routeEntry2));
+    }
+
+    /**
+     * Tests non-equality of {@link RouteEntry}.
+     */
+    @Test
+    public void testNonEquality() {
+        IpPrefix prefix1 = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop1 = IpAddress.valueOf("5.6.7.8");
+        RouteEntry routeEntry1 = new RouteEntry(prefix1, nextHop1);
+
+        IpPrefix prefix2 = IpPrefix.valueOf("1.2.3.0/25");        // Different
+        IpAddress nextHop2 = IpAddress.valueOf("5.6.7.8");
+        RouteEntry routeEntry2 = new RouteEntry(prefix2, nextHop2);
+
+        IpPrefix prefix3 = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop3 = IpAddress.valueOf("5.6.7.9");        // Different
+        RouteEntry routeEntry3 = new RouteEntry(prefix3, nextHop3);
+
+        assertThat(routeEntry1, is(not(routeEntry2)));
+        assertThat(routeEntry1, is(not(routeEntry3)));
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+        RouteEntry routeEntry = new RouteEntry(prefix, nextHop);
+
+        assertThat(routeEntry.toString(),
+                   is("RouteEntry{prefix=1.2.3.0/24, nextHop=5.6.7.8}"));
+    }
+}
diff --git a/cli/pom.xml b/cli/pom.xml
index cc1d9dd..8a11f34 100644
--- a/cli/pom.xml
+++ b/cli/pom.xml
@@ -26,6 +26,16 @@
             <groupId>org.onlab.onos</groupId>
             <artifactId>onlab-osgi</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.core</artifactId>
diff --git a/cli/src/main/java/org/onlab/onos/cli/AbstractShellCommand.java b/cli/src/main/java/org/onlab/onos/cli/AbstractShellCommand.java
index 184a7e6..839d2841 100644
--- a/cli/src/main/java/org/onlab/onos/cli/AbstractShellCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/AbstractShellCommand.java
@@ -1,5 +1,6 @@
 package org.onlab.onos.cli;
 
+import org.apache.karaf.shell.commands.Option;
 import org.apache.karaf.shell.console.OsgiCommandSupport;
 import org.onlab.osgi.DefaultServiceDirectory;
 import org.onlab.osgi.ServiceNotFoundException;
@@ -9,6 +10,10 @@
  */
 public abstract class AbstractShellCommand extends OsgiCommandSupport {
 
+    @Option(name = "-j", aliases = "--json", description = "Output JSON",
+            required = false, multiValued = false)
+    private boolean json = false;
+
     /**
      * Returns the reference to the implementation of the specified service.
      *
@@ -46,6 +51,15 @@
      */
     protected abstract void execute();
 
+    /**
+     * Indicates whether JSON format should be output.
+     *
+     * @return true if JSON is requested
+     */
+    protected boolean outputJson() {
+        return json;
+    }
+
     @Override
     protected Object doExecute() throws Exception {
         try {
diff --git a/cli/src/main/java/org/onlab/onos/cli/MastersListCommand.java b/cli/src/main/java/org/onlab/onos/cli/MastersListCommand.java
index 0f7857f..fff4955 100644
--- a/cli/src/main/java/org/onlab/onos/cli/MastersListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/MastersListCommand.java
@@ -1,7 +1,9 @@
 package org.onlab.onos.cli;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.google.common.collect.Lists;
-
 import org.apache.karaf.shell.commands.Command;
 import org.onlab.onos.cluster.ClusterService;
 import org.onlab.onos.cluster.ControllerNode;
@@ -26,15 +28,50 @@
         MastershipService mastershipService = get(MastershipService.class);
         List<ControllerNode> nodes = newArrayList(service.getNodes());
         Collections.sort(nodes, Comparators.NODE_COMPARATOR);
+
+        if (outputJson()) {
+            print("%s", json(service, mastershipService, nodes));
+        } else {
+            for (ControllerNode node : nodes) {
+                List<DeviceId> ids = Lists.newArrayList(mastershipService.getDevicesOf(node.id()));
+                Collections.sort(ids, Comparators.ELEMENT_ID_COMPARATOR);
+                print("%s: %d devices", node.id(), ids.size());
+                for (DeviceId deviceId : ids) {
+                    print("  %s", deviceId);
+                }
+            }
+        }
+    }
+
+    // Produces JSON structure.
+    private JsonNode json(ClusterService service, MastershipService mastershipService,
+                          List<ControllerNode> nodes) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
         ControllerNode self = service.getLocalNode();
         for (ControllerNode node : nodes) {
             List<DeviceId> ids = Lists.newArrayList(mastershipService.getDevicesOf(node.id()));
-            Collections.sort(ids, Comparators.ELEMENT_ID_COMPARATOR);
-            print("%s: %d devices", node.id(), ids.size());
-            for (DeviceId deviceId : ids) {
-                print("  %s", deviceId);
-            }
+            result.add(mapper.createObjectNode()
+                               .put("id", node.id().toString())
+                               .put("size", ids.size())
+                               .set("devices", json(mapper, ids)));
         }
+        return result;
+    }
+
+    /**
+     * Produces a JSON array containing the specified device identifiers.
+     *
+     * @param mapper object mapper
+     * @param ids    collection of device identifiers
+     * @return JSON array
+     */
+    public static JsonNode json(ObjectMapper mapper, Iterable<DeviceId> ids) {
+        ArrayNode result = mapper.createArrayNode();
+        for (DeviceId deviceId : ids) {
+            result.add(deviceId.toString());
+        }
+        return result;
     }
 
 }
diff --git a/cli/src/main/java/org/onlab/onos/cli/NodesListCommand.java b/cli/src/main/java/org/onlab/onos/cli/NodesListCommand.java
index b7b4556..d9bdf94 100644
--- a/cli/src/main/java/org/onlab/onos/cli/NodesListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/NodesListCommand.java
@@ -1,5 +1,8 @@
 package org.onlab.onos.cli;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
 import org.apache.karaf.shell.commands.Command;
 import org.onlab.onos.cluster.ClusterService;
 import org.onlab.onos.cluster.ControllerNode;
@@ -24,12 +27,32 @@
         ClusterService service = get(ClusterService.class);
         List<ControllerNode> nodes = newArrayList(service.getNodes());
         Collections.sort(nodes, Comparators.NODE_COMPARATOR);
+        if (outputJson()) {
+            print("%s", json(service, nodes));
+        } else {
+            ControllerNode self = service.getLocalNode();
+            for (ControllerNode node : nodes) {
+                print(FMT, node.id(), node.ip(), node.tcpPort(),
+                      service.getState(node.id()),
+                      node.equals(self) ? "*" : "");
+            }
+        }
+    }
+
+    // Produces JSON structure.
+    private JsonNode json(ClusterService service, List<ControllerNode> nodes) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
         ControllerNode self = service.getLocalNode();
         for (ControllerNode node : nodes) {
-            print(FMT, node.id(), node.ip(), node.tcpPort(),
-                  service.getState(node.id()),
-                  node.equals(self) ? "*" : "");
+            result.add(mapper.createObjectNode()
+                               .put("id", node.id().toString())
+                               .put("ip", node.ip().toString())
+                               .put("tcpPort", node.tcpPort())
+                               .put("state", service.getState(node.id()).toString())
+                               .put("self", node.equals(self)));
         }
+        return result;
     }
 
 }
diff --git a/cli/src/main/java/org/onlab/onos/cli/SummaryCommand.java b/cli/src/main/java/org/onlab/onos/cli/SummaryCommand.java
index 1597b55..180405b 100644
--- a/cli/src/main/java/org/onlab/onos/cli/SummaryCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/SummaryCommand.java
@@ -1,5 +1,6 @@
 package org.onlab.onos.cli;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.karaf.shell.commands.Command;
 import org.onlab.onos.CoreService;
 import org.onlab.onos.cluster.ClusterService;
@@ -22,18 +23,32 @@
     protected void execute() {
         TopologyService topologyService = get(TopologyService.class);
         Topology topology = topologyService.currentTopology();
-        print("node=%s, version=%s",
-              get(ClusterService.class).getLocalNode().ip(),
-              get(CoreService.class).version().toString());
-        print("nodes=%d, devices=%d, links=%d, hosts=%d, clusters=%s, paths=%d, flows=%d, intents=%d",
-              get(ClusterService.class).getNodes().size(),
-              get(DeviceService.class).getDeviceCount(),
-              get(LinkService.class).getLinkCount(),
-              get(HostService.class).getHostCount(),
-              topologyService.getClusters(topology).size(),
-              topology.pathCount(),
-              get(FlowRuleService.class).getFlowRuleCount(),
-              get(IntentService.class).getIntentCount());
+        if (outputJson()) {
+            print("%s", new ObjectMapper().createObjectNode()
+                    .put("node", get(ClusterService.class).getLocalNode().ip().toString())
+                    .put("version", get(CoreService.class).version().toString())
+                    .put("nodes", get(ClusterService.class).getNodes().size())
+                    .put("devices", get(DeviceService.class).getDeviceCount())
+                    .put("links", get(LinkService.class).getLinkCount())
+                    .put("hosts", get(HostService.class).getHostCount())
+                    .put("clusters", topologyService.getClusters(topology).size())
+                    .put("paths", topology.pathCount())
+                    .put("flows", get(FlowRuleService.class).getFlowRuleCount())
+                    .put("intents", get(IntentService.class).getIntentCount()));
+        } else {
+            print("node=%s, version=%s",
+                  get(ClusterService.class).getLocalNode().ip(),
+                  get(CoreService.class).version().toString());
+            print("nodes=%d, devices=%d, links=%d, hosts=%d, clusters=%s, paths=%d, flows=%d, intents=%d",
+                  get(ClusterService.class).getNodes().size(),
+                  get(DeviceService.class).getDeviceCount(),
+                  get(LinkService.class).getLinkCount(),
+                  get(HostService.class).getHostCount(),
+                  topologyService.getClusters(topology).size(),
+                  topology.pathCount(),
+                  get(FlowRuleService.class).getFlowRuleCount(),
+                  get(IntentService.class).getIntentCount());
+        }
     }
 
 }
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/ClusterDevicesCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/ClusterDevicesCommand.java
index f09b185..e03af45 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/ClusterDevicesCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/ClusterDevicesCommand.java
@@ -1,5 +1,6 @@
 package org.onlab.onos.cli.net;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.Lists;
 import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
@@ -10,6 +11,7 @@
 import java.util.Collections;
 import java.util.List;
 
+import static org.onlab.onos.cli.MastersListCommand.json;
 import static org.onlab.onos.net.topology.ClusterId.clusterId;
 
 /**
@@ -33,11 +35,14 @@
         } else {
             List<DeviceId> ids = Lists.newArrayList(service.getClusterDevices(topology, cluster));
             Collections.sort(ids, Comparators.ELEMENT_ID_COMPARATOR);
-            for (DeviceId deviceId : ids) {
-                print("%s", deviceId);
+            if (outputJson()) {
+                print("%s", json(new ObjectMapper(), ids));
+            } else {
+                for (DeviceId deviceId : ids) {
+                    print("%s", deviceId);
+                }
             }
         }
     }
 
-
 }
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/ClusterLinksCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/ClusterLinksCommand.java
index 2bbfb46..ed5be77 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/ClusterLinksCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/ClusterLinksCommand.java
@@ -5,6 +5,7 @@
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.topology.TopologyCluster;
 
+import static org.onlab.onos.cli.net.LinksListCommand.json;
 import static org.onlab.onos.cli.net.LinksListCommand.linkString;
 import static org.onlab.onos.net.topology.ClusterId.clusterId;
 
@@ -26,6 +27,8 @@
         TopologyCluster cluster = service.getCluster(topology, clusterId(cid));
         if (cluster == null) {
             error("No such cluster %s", cid);
+        } else if (outputJson()) {
+            print("%s", json(service.getClusterLinks(topology, cluster)));
         } else {
             for (Link link : service.getClusterLinks(topology, cluster)) {
                 print(linkString(link));
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/ClustersListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/ClustersListCommand.java
index 2b2953b..f41f85e 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/ClustersListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/ClustersListCommand.java
@@ -1,5 +1,8 @@
 package org.onlab.onos.cli.net;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.google.common.collect.Lists;
 import org.apache.karaf.shell.commands.Command;
 import org.onlab.onos.cli.Comparators;
@@ -24,9 +27,26 @@
         List<TopologyCluster> clusters = Lists.newArrayList(service.getClusters(topology));
         Collections.sort(clusters, Comparators.CLUSTER_COMPARATOR);
 
-        for (TopologyCluster cluster : clusters) {
-            print(FMT, cluster.id().index(), cluster.deviceCount(), cluster.linkCount());
+        if (outputJson()) {
+            print("%s", json(clusters));
+        } else {
+            for (TopologyCluster cluster : clusters) {
+                print(FMT, cluster.id().index(), cluster.deviceCount(), cluster.linkCount());
+            }
         }
     }
 
+    // Produces a JSON result.
+    private JsonNode json(Iterable<TopologyCluster> clusters) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        for (TopologyCluster cluster : clusters) {
+            result.add(mapper.createObjectNode()
+                               .put("id", cluster.id().index())
+                               .put("deviceCount", cluster.deviceCount())
+                               .put("linkCount", cluster.linkCount()));
+        }
+        return result;
+    }
+
 }
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java
index f66cedd..32b7830 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java
@@ -1,7 +1,12 @@
 package org.onlab.onos.cli.net;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
+import org.apache.karaf.shell.commands.Option;
 import org.onlab.onos.cli.Comparators;
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.Port;
@@ -22,6 +27,14 @@
 
     private static final String FMT = "  port=%s, state=%s";
 
+    @Option(name = "-e", aliases = "--enabled", description = "Show only enabled ports",
+            required = false, multiValued = false)
+    private boolean enabled = false;
+
+    @Option(name = "-d", aliases = "--disabled", description = "Show only disabled ports",
+            required = false, multiValued = false)
+    private boolean disabled = false;
+
     @Argument(index = 0, name = "uri", description = "Device ID",
               required = false, multiValued = false)
     String uri = null;
@@ -30,26 +43,78 @@
     protected void execute() {
         DeviceService service = get(DeviceService.class);
         if (uri == null) {
-            for (Device device : getSortedDevices(service)) {
-                printDevice(service, device);
+            if (outputJson()) {
+                print("%s", jsonPorts(service, getSortedDevices(service)));
+            } else {
+                for (Device device : getSortedDevices(service)) {
+                    printDevice(service, device);
+                }
             }
+
         } else {
             Device device = service.getDevice(deviceId(uri));
             if (device == null) {
                 error("No such device %s", uri);
+            } else if (outputJson()) {
+                print("%s", jsonPorts(service, new ObjectMapper(), device));
             } else {
                 printDevice(service, device);
             }
         }
     }
 
+    /**
+     * Produces JSON array containing ports of the specified devices.
+     *
+     * @param service device service
+     * @param devices collection of devices
+     * @return JSON array
+     */
+    public JsonNode jsonPorts(DeviceService service, Iterable<Device> devices) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        for (Device device : devices) {
+            result.add(jsonPorts(service, mapper, device));
+        }
+        return result;
+    }
+
+    /**
+     * Produces JSON array containing ports of the specified device.
+     *
+     * @param service device service
+     * @param mapper  object mapper
+     * @param device  infrastructure devices
+     * @return JSON array
+     */
+    public JsonNode jsonPorts(DeviceService service, ObjectMapper mapper, Device device) {
+        ObjectNode result = mapper.createObjectNode();
+        ArrayNode ports = mapper.createArrayNode();
+        for (Port port : service.getPorts(device.id())) {
+            if (isIncluded(port)) {
+                ports.add(mapper.createObjectNode()
+                                  .put("port", port.number().toString())
+                                  .put("isEnabled", port.isEnabled()));
+            }
+        }
+        return result.put("device", device.id().toString()).set("ports", ports);
+    }
+
+    // Determines if a port should be included in output.
+    private boolean isIncluded(Port port) {
+        return enabled && port.isEnabled() || disabled && !port.isEnabled() ||
+                !enabled && !disabled;
+    }
+
     @Override
     protected void printDevice(DeviceService service, Device device) {
         super.printDevice(service, device);
         List<Port> ports = new ArrayList<>(service.getPorts(device.id()));
         Collections.sort(ports, Comparators.PORT_COMPARATOR);
         for (Port port : ports) {
-            print(FMT, port.number(), port.isEnabled() ? "enabled" : "disabled");
+            if (isIncluded(port)) {
+                print(FMT, port.number(), port.isEnabled() ? "enabled" : "disabled");
+            }
         }
     }
 
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/DevicesListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/DevicesListCommand.java
index f34f97e..b7a8acc 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/DevicesListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/DevicesListCommand.java
@@ -1,5 +1,9 @@
 package org.onlab.onos.cli.net;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.karaf.shell.commands.Command;
 import org.onlab.onos.cli.AbstractShellCommand;
 import org.onlab.onos.cli.Comparators;
@@ -24,12 +28,55 @@
     @Override
     protected void execute() {
         DeviceService service = get(DeviceService.class);
-        for (Device device : getSortedDevices(service)) {
-            printDevice(service, device);
+        if (outputJson()) {
+            print("%s", json(service, getSortedDevices(service)));
+        } else {
+            for (Device device : getSortedDevices(service)) {
+                printDevice(service, device);
+            }
         }
     }
 
     /**
+     * Returns JSON node representing the specified devices.
+     *
+     * @param service device service
+     * @param devices collection of devices
+     * @return JSON node
+     */
+    public static JsonNode json(DeviceService service, Iterable<Device> devices) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        for (Device device : devices) {
+            result.add(json(service, mapper, device));
+        }
+        return result;
+    }
+
+    /**
+     * Returns JSON node representing the specified device.
+     *
+     * @param service device service
+     * @param mapper  object mapper
+     * @param device  infrastructure device
+     * @return JSON node
+     */
+    public static ObjectNode json(DeviceService service, ObjectMapper mapper,
+                                  Device device) {
+        ObjectNode result = mapper.createObjectNode();
+        if (device != null) {
+            result.put("id", device.id().toString())
+                    .put("available", service.isAvailable(device.id()))
+                    .put("role", service.getRole(device.id()).toString())
+                    .put("mfr", device.manufacturer())
+                    .put("hw", device.hwVersion())
+                    .put("sw", device.swVersion())
+                    .put("serial", device.serialNumber());
+        }
+        return result;
+    }
+
+    /**
      * Returns the list of devices sorted using the device ID URIs.
      *
      * @param service device service
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/FlowsListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/FlowsListCommand.java
index 28309c5..9291212 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/FlowsListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/FlowsListCommand.java
@@ -1,5 +1,9 @@
 package org.onlab.onos.cli.net;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.collect.Maps;
 import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
@@ -12,6 +16,8 @@
 import org.onlab.onos.net.flow.FlowEntry;
 import org.onlab.onos.net.flow.FlowEntry.FlowEntryState;
 import org.onlab.onos.net.flow.FlowRuleService;
+import org.onlab.onos.net.flow.criteria.Criterion;
+import org.onlab.onos.net.flow.instructions.Instruction;
 
 import java.util.Collections;
 import java.util.List;
@@ -48,12 +54,76 @@
         DeviceService deviceService = get(DeviceService.class);
         FlowRuleService service = get(FlowRuleService.class);
         Map<Device, List<FlowEntry>> flows = getSortedFlows(deviceService, service);
-        for (Device d : getSortedDevices(deviceService)) {
-            printFlows(d, flows.get(d), coreService);
+
+        if (outputJson()) {
+            print("%s", json(coreService, getSortedDevices(deviceService), flows));
+        } else {
+            for (Device d : getSortedDevices(deviceService)) {
+                printFlows(d, flows.get(d), coreService);
+            }
         }
     }
 
     /**
+     * Produces a JSON array of flows grouped by the each device.
+     *
+     * @param coreService core service
+     * @param devices     collection of devices to group flow by
+     * @param flows       collection of flows per each device
+     * @return JSON array
+     */
+    private JsonNode json(CoreService coreService, Iterable<Device> devices,
+                          Map<Device, List<FlowEntry>> flows) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        for (Device device : devices) {
+            result.add(json(coreService, mapper, device, flows.get(device)));
+        }
+        return result;
+    }
+
+    // Produces JSON object with the flows of the given device.
+    private ObjectNode json(CoreService coreService, ObjectMapper mapper,
+                            Device device, List<FlowEntry> flows) {
+        ObjectNode result = mapper.createObjectNode();
+        ArrayNode array = mapper.createArrayNode();
+
+        for (FlowEntry flow : flows) {
+            array.add(json(coreService, mapper, flow));
+        }
+
+        result.put("device", device.id().toString())
+                .put("flowCount", flows.size())
+                .put("flows", array);
+        return result;
+    }
+
+    // Produces JSON structure with the specified flow data.
+    private ObjectNode json(CoreService coreService, ObjectMapper mapper,
+                            FlowEntry flow) {
+        ObjectNode result = mapper.createObjectNode();
+        ArrayNode crit = mapper.createArrayNode();
+        for (Criterion c : flow.selector().criteria()) {
+            crit.add(c.toString());
+        }
+
+        ArrayNode instr = mapper.createArrayNode();
+        for (Instruction i : flow.treatment().instructions()) {
+            instr.add(i.toString());
+        }
+
+        result.put("flowId", Long.toHexString(flow.id().value()))
+                .put("state", flow.state().toString())
+                .put("bytes", flow.bytes())
+                .put("packets", flow.packets())
+                .put("life", flow.life())
+                .put("appId", coreService.getAppId(flow.appId()).name());
+        result.set("selector", crit);
+        result.set("treatment", instr);
+        return result;
+    }
+
+    /**
      * Returns the list of devices sorted using the device ID URIs.
      *
      * @param service device service
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/HostsListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/HostsListCommand.java
index cd9ba08..f431142 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/HostsListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/HostsListCommand.java
@@ -1,10 +1,15 @@
 package org.onlab.onos.cli.net;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.karaf.shell.commands.Command;
 import org.onlab.onos.cli.AbstractShellCommand;
 import org.onlab.onos.cli.Comparators;
 import org.onlab.onos.net.Host;
 import org.onlab.onos.net.host.HostService;
+import org.onlab.packet.IpPrefix;
 
 import java.util.Collections;
 import java.util.List;
@@ -15,7 +20,7 @@
  * Lists all currently-known hosts.
  */
 @Command(scope = "onos", name = "hosts",
-        description = "Lists all currently-known hosts.")
+         description = "Lists all currently-known hosts.")
 public class HostsListCommand extends AbstractShellCommand {
 
     private static final String FMT =
@@ -24,11 +29,42 @@
     @Override
     protected void execute() {
         HostService service = get(HostService.class);
-        for (Host host : getSortedHosts(service)) {
-            printHost(host);
+        if (outputJson()) {
+            print("%s", json(getSortedHosts(service)));
+        } else {
+            for (Host host : getSortedHosts(service)) {
+                printHost(host);
+            }
         }
     }
 
+    // Produces JSON structure.
+    private static JsonNode json(Iterable<Host> hosts) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        for (Host host : hosts) {
+            result.add(json(mapper, host));
+        }
+        return result;
+    }
+
+    // Produces JSON structure.
+    private static JsonNode json(ObjectMapper mapper, Host host) {
+        ObjectNode loc = LinksListCommand.json(mapper, host.location())
+                .put("time", host.location().time());
+        ArrayNode ips = mapper.createArrayNode();
+        for (IpPrefix ip : host.ipAddresses()) {
+            ips.add(ip.toString());
+        }
+        ObjectNode result = mapper.createObjectNode()
+                .put("id", host.id().toString())
+                .put("mac", host.mac().toString())
+                .put("vlan", host.vlan().toString());
+        result.set("location", loc);
+        result.set("ips", ips);
+        return result;
+    }
+
     /**
      * Returns the list of devices sorted using the device ID URIs.
      *
@@ -44,14 +80,14 @@
     /**
      * Prints information about a host.
      *
-     * @param host
+     * @param host end-station host
      */
     protected void printHost(Host host) {
         if (host != null) {
             print(FMT, host.id(), host.mac(),
-                    host.location().deviceId(),
-                    host.location().port(),
-                    host.vlan(), host.ipAddresses());
+                  host.location().deviceId(),
+                  host.location().port(),
+                  host.vlan(), host.ipAddresses());
         }
     }
- }
+}
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/LinksListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/LinksListCommand.java
index f5226b1..1c3287e 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/LinksListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/LinksListCommand.java
@@ -1,8 +1,13 @@
 package org.onlab.onos.cli.net;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
 import org.onlab.onos.cli.AbstractShellCommand;
+import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.link.LinkService;
 
@@ -27,12 +32,58 @@
         LinkService service = get(LinkService.class);
         Iterable<Link> links = uri != null ?
                 service.getDeviceLinks(deviceId(uri)) : service.getLinks();
-        for (Link link : links) {
-            print(linkString(link));
+        if (outputJson()) {
+            print("%s", json(links));
+        } else {
+            for (Link link : links) {
+                print(linkString(link));
+            }
         }
     }
 
     /**
+     * Produces a JSON array containing the specified links.
+     *
+     * @param links collection of links
+     * @return JSON array
+     */
+    public static JsonNode json(Iterable<Link> links) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        for (Link link : links) {
+            result.add(json(mapper, link));
+        }
+        return result;
+    }
+
+    /**
+     * Produces a JSON object for the specified link.
+     *
+     * @param mapper object mapper
+     * @param link   link to encode
+     * @return JSON object
+     */
+    public static ObjectNode json(ObjectMapper mapper, Link link) {
+        ObjectNode result = mapper.createObjectNode();
+        result.set("src", json(mapper, link.src()));
+        result.set("dst", json(mapper, link.dst()));
+        return result;
+    }
+
+    /**
+     * Produces a JSON object for the specified connect point.
+     *
+     * @param mapper       object mapper
+     * @param connectPoint connection point to encode
+     * @return JSON object
+     */
+    public static ObjectNode json(ObjectMapper mapper, ConnectPoint connectPoint) {
+        return mapper.createObjectNode()
+                .put("device", connectPoint.deviceId().toString())
+                .put("port", connectPoint.port().toString());
+    }
+
+    /**
      * Returns a formatted string representing the given link.
      *
      * @param link infrastructure link
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/PathListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/PathListCommand.java
index 8bb808a..6777625 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/PathListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/PathListCommand.java
@@ -1,5 +1,8 @@
 package org.onlab.onos.cli.net;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
 import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
 import org.onlab.onos.net.Link;
@@ -32,12 +35,33 @@
     protected void execute() {
         init();
         Set<Path> paths = service.getPaths(topology, deviceId(src), deviceId(dst));
-        for (Path path : paths) {
-            print(pathString(path));
+        if (outputJson()) {
+            print("%s", json(paths));
+        } else {
+            for (Path path : paths) {
+                print(pathString(path));
+            }
         }
     }
 
     /**
+     * Produces a JSON array containing the specified paths.
+     *
+     * @param paths collection of paths
+     * @return JSON array
+     */
+    public static JsonNode json(Iterable<Path> paths) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        for (Path path : paths) {
+            result.add(LinksListCommand.json(mapper, path)
+                               .put("cost", path.cost())
+                               .set("links", LinksListCommand.json(path.links())));
+        }
+        return result;
+    }
+
+    /**
      * Produces a formatted string representing the specified path.
      *
      * @param path network path
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/TopologyCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/TopologyCommand.java
index 5c8310f..a7e6422 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/TopologyCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/TopologyCommand.java
@@ -1,5 +1,6 @@
 package org.onlab.onos.cli.net;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.karaf.shell.commands.Command;
 import org.onlab.onos.cli.AbstractShellCommand;
 import org.onlab.onos.net.topology.Topology;
@@ -30,8 +31,17 @@
     @Override
     protected void execute() {
         init();
-        print(FMT, topology.time(), topology.deviceCount(), topology.linkCount(),
-              topology.clusterCount(), topology.pathCount());
+        if (outputJson()) {
+            print("%s", new ObjectMapper().createObjectNode()
+                    .put("time", topology.time())
+                    .put("deviceCount", topology.deviceCount())
+                    .put("linkCount", topology.linkCount())
+                    .put("clusterCount", topology.clusterCount())
+                    .put("pathCount", topology.pathCount()));
+        } else {
+            print(FMT, topology.time(), topology.deviceCount(), topology.linkCount(),
+                  topology.clusterCount(), topology.pathCount());
+        }
     }
 
 }
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/ControllerNodeToNodeId.java b/core/api/src/main/java/org/onlab/onos/cluster/ControllerNodeToNodeId.java
index 0891494..0f79c68 100644
--- a/core/api/src/main/java/org/onlab/onos/cluster/ControllerNodeToNodeId.java
+++ b/core/api/src/main/java/org/onlab/onos/cluster/ControllerNodeToNodeId.java
@@ -12,7 +12,11 @@
 
     @Override
     public NodeId apply(ControllerNode input) {
-        return input.id();
+        if (input == null) {
+            return null;
+        } else {
+            return input.id();
+        }
     }
 
     /**
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/DefaultHostDescription.java b/core/api/src/main/java/org/onlab/onos/net/host/DefaultHostDescription.java
index 2e92dad..71a952e 100644
--- a/core/api/src/main/java/org/onlab/onos/net/host/DefaultHostDescription.java
+++ b/core/api/src/main/java/org/onlab/onos/net/host/DefaultHostDescription.java
@@ -1,5 +1,8 @@
 package org.onlab.onos.net.host;
 
+import java.util.Collections;
+import java.util.Set;
+
 import org.onlab.onos.net.AbstractDescription;
 import org.onlab.onos.net.HostLocation;
 import org.onlab.onos.net.SparseAnnotations;
@@ -7,6 +10,8 @@
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
 
+import com.google.common.collect.ImmutableSet;
+
 import static com.google.common.base.MoreObjects.toStringHelper;
 
 /**
@@ -18,7 +23,7 @@
     private final MacAddress mac;
     private final VlanId vlan;
     private final HostLocation location;
-    private final IpPrefix ip;
+    private final Set<IpPrefix> ip;
 
     /**
      * Creates a host description using the supplied information.
@@ -31,7 +36,7 @@
     public DefaultHostDescription(MacAddress mac, VlanId vlan,
                                   HostLocation location,
                                   SparseAnnotations... annotations) {
-        this(mac, vlan, location, null, annotations);
+        this(mac, vlan, location, Collections.<IpPrefix>emptySet(), annotations);
     }
 
     /**
@@ -46,11 +51,26 @@
     public DefaultHostDescription(MacAddress mac, VlanId vlan,
                                   HostLocation location, IpPrefix ip,
                                   SparseAnnotations... annotations) {
+        this(mac, vlan, location, ImmutableSet.of(ip), annotations);
+    }
+
+    /**
+     * Creates a host description using the supplied information.
+     *
+     * @param mac         host MAC address
+     * @param vlan        host VLAN identifier
+     * @param location    host location
+     * @param ip          host IP addresses
+     * @param annotations optional key/value annotations map
+     */
+    public DefaultHostDescription(MacAddress mac, VlanId vlan,
+                                  HostLocation location, Set<IpPrefix> ip,
+                                  SparseAnnotations... annotations) {
         super(annotations);
         this.mac = mac;
         this.vlan = vlan;
         this.location = location;
-        this.ip = ip;
+        this.ip = ImmutableSet.copyOf(ip);
     }
 
     @Override
@@ -69,7 +89,7 @@
     }
 
     @Override
-    public IpPrefix ipAddress() {
+    public Set<IpPrefix> ipAddress() {
         return ip;
     }
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostDescription.java b/core/api/src/main/java/org/onlab/onos/net/host/HostDescription.java
index fc16854..258ce3d 100644
--- a/core/api/src/main/java/org/onlab/onos/net/host/HostDescription.java
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostDescription.java
@@ -1,5 +1,7 @@
 package org.onlab.onos.net.host;
 
+import java.util.Set;
+
 import org.onlab.onos.net.Description;
 import org.onlab.onos.net.HostLocation;
 import org.onlab.packet.IpPrefix;
@@ -38,6 +40,6 @@
      * @return host IP address
      */
     // FIXME: Switch to IpAddress
-    IpPrefix ipAddress();
+    Set<IpPrefix> ipAddress();
 
 }
diff --git a/core/api/src/test/java/org/onlab/onos/cluster/ControllerNodeToNodeIdTest.java b/core/api/src/test/java/org/onlab/onos/cluster/ControllerNodeToNodeIdTest.java
index 44261e8..b95dcfc 100644
--- a/core/api/src/test/java/org/onlab/onos/cluster/ControllerNodeToNodeIdTest.java
+++ b/core/api/src/test/java/org/onlab/onos/cluster/ControllerNodeToNodeIdTest.java
@@ -1,5 +1,6 @@
 package org.onlab.onos.cluster;
 
+import static com.google.common.base.Predicates.notNull;
 import static org.junit.Assert.*;
 import static org.onlab.onos.cluster.ControllerNodeToNodeId.toNodeId;
 
@@ -30,12 +31,13 @@
     @Test
     public final void testToNodeId() {
 
-        final Iterable<ControllerNode> nodes = Arrays.asList(CN1, CN2, CN3);
+        final Iterable<ControllerNode> nodes = Arrays.asList(CN1, CN2, CN3, null);
         final List<NodeId> nodeIds = Arrays.asList(NID1, NID2, NID3);
 
         assertEquals(nodeIds,
                 FluentIterable.from(nodes)
                     .transform(toNodeId())
+                    .filter(notNull())
                     .toList());
     }
 
diff --git a/core/api/src/test/java/org/onlab/onos/net/host/DefualtHostDecriptionTest.java b/core/api/src/test/java/org/onlab/onos/net/host/DefualtHostDecriptionTest.java
index 5ae7c27..f2b9475 100644
--- a/core/api/src/test/java/org/onlab/onos/net/host/DefualtHostDecriptionTest.java
+++ b/core/api/src/test/java/org/onlab/onos/net/host/DefualtHostDecriptionTest.java
@@ -8,6 +8,8 @@
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
 
+import com.google.common.collect.ImmutableSet;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -33,7 +35,7 @@
         assertEquals("incorrect mac", MAC, host.hwAddress());
         assertEquals("incorrect vlan", VLAN, host.vlan());
         assertEquals("incorrect location", LOC, host.location());
-        assertEquals("incorrect ip's", IP, host.ipAddress());
+        assertEquals("incorrect ip's", ImmutableSet.of(IP), host.ipAddress());
         assertTrue("incorrect toString", host.toString().contains("vlan=10"));
     }
 
diff --git a/core/net/src/main/java/org/onlab/onos/impl/package-info.java b/core/net/src/main/java/org/onlab/onos/impl/package-info.java
index bbe539f..2bf17b3 100644
--- a/core/net/src/main/java/org/onlab/onos/impl/package-info.java
+++ b/core/net/src/main/java/org/onlab/onos/impl/package-info.java
@@ -1,4 +1,4 @@
 /**
- *
+ * Miscellaneous core system implementations.
  */
 package org.onlab.onos.impl;
\ No newline at end of file
diff --git a/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java b/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java
index 8a86544..cd84dce 100644
--- a/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java
@@ -355,7 +355,7 @@
 
         arp.setTargetProtocolAddress(((ARP) request.getPayload())
                 .getSenderProtocolAddress());
-        arp.setSenderProtocolAddress(srcIp.toRealInt());
+        arp.setSenderProtocolAddress(srcIp.toInt());
         eth.setPayload(arp);
         return eth;
     }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
index d923075..2603da1 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
@@ -515,12 +515,12 @@
                                             Map<PortNumber, Port> ports,
                                             Set<PortNumber> processed) {
         List<DeviceEvent> events = new ArrayList<>();
-        Iterator<PortNumber> iterator = ports.keySet().iterator();
+        Iterator<Entry<PortNumber, Port>> iterator = ports.entrySet().iterator();
         while (iterator.hasNext()) {
-            PortNumber portNumber = iterator.next();
+            Entry<PortNumber, Port> e = iterator.next();
+            PortNumber portNumber = e.getKey();
             if (!processed.contains(portNumber)) {
-                events.add(new DeviceEvent(PORT_REMOVED, device,
-                                           ports.get(portNumber)));
+                events.add(new DeviceEvent(PORT_REMOVED, device, e.getValue()));
                 iterator.remove();
             }
         }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java
index 39bc770..4025d0c 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java
@@ -1,10 +1,13 @@
 package org.onlab.onos.store.host.impl;
 
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 
+import org.apache.commons.lang3.RandomUtils;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -12,6 +15,8 @@
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
 import org.onlab.onos.cluster.ClusterService;
+import org.onlab.onos.cluster.ControllerNode;
+import org.onlab.onos.cluster.NodeId;
 import org.onlab.onos.net.Annotations;
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.DefaultHost;
@@ -19,6 +24,7 @@
 import org.onlab.onos.net.Host;
 import org.onlab.onos.net.HostId;
 import org.onlab.onos.net.HostLocation;
+import org.onlab.onos.net.host.DefaultHostDescription;
 import org.onlab.onos.net.host.HostClockService;
 import org.onlab.onos.net.host.HostDescription;
 import org.onlab.onos.net.host.HostEvent;
@@ -42,12 +48,19 @@
 import org.slf4j.Logger;
 
 import java.io.IOException;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 
+import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
+import static org.onlab.onos.cluster.ControllerNodeToNodeId.toNodeId;
 import static org.onlab.onos.net.host.HostEvent.Type.*;
+import static org.onlab.util.Tools.namedThreads;
 import static org.slf4j.LoggerFactory.getLogger;
 
 //TODO: multi-provider, annotation not supported.
@@ -88,24 +101,58 @@
         protected void setupKryoPool() {
             serializerPool = KryoPool.newBuilder()
                     .register(DistributedStoreSerializers.COMMON)
+                    .register(InternalHostEvent.class)
                     .register(InternalHostRemovedEvent.class)
+                    .register(HostFragmentId.class)
+                    .register(HostAntiEntropyAdvertisement.class)
                     .build()
                     .populate(1);
         }
     };
 
+    private ScheduledExecutorService executor;
+
     @Activate
     public void activate() {
         clusterCommunicator.addSubscriber(
-                GossipHostStoreMessageSubjects.HOST_UPDATED, new InternalHostEventListener());
+                GossipHostStoreMessageSubjects.HOST_UPDATED,
+                new InternalHostEventListener());
         clusterCommunicator.addSubscriber(
-                GossipHostStoreMessageSubjects.HOST_REMOVED, new InternalHostRemovedEventListener());
+                GossipHostStoreMessageSubjects.HOST_REMOVED,
+                new InternalHostRemovedEventListener());
+        clusterCommunicator.addSubscriber(
+                GossipHostStoreMessageSubjects.HOST_ANTI_ENTROPY_ADVERTISEMENT,
+                new InternalHostAntiEntropyAdvertisementListener());
+
+        executor =
+                newSingleThreadScheduledExecutor(namedThreads("link-anti-entropy-%d"));
+
+        // TODO: Make these configurable
+        long initialDelaySec = 5;
+        long periodSec = 5;
+        // start anti-entropy thread
+        executor.scheduleAtFixedRate(new SendAdvertisementTask(),
+                    initialDelaySec, periodSec, TimeUnit.SECONDS);
 
         log.info("Started");
     }
 
     @Deactivate
     public void deactivate() {
+        executor.shutdownNow();
+        try {
+            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
+                log.error("Timeout during executor shutdown");
+            }
+        } catch (InterruptedException e) {
+            log.error("Error during executor shutdown", e);
+        }
+
+        hosts.clear();
+        removedHosts.clear();
+        locations.clear();
+        portAddresses.clear();
+
         log.info("Stopped");
     }
 
@@ -153,7 +200,7 @@
                     descr.hwAddress(),
                     descr.vlan(),
                     new Timestamped<>(descr.location(), timestamp),
-                    ImmutableSet.of(descr.ipAddress()));
+                    ImmutableSet.copyOf(descr.ipAddress()));
             hosts.put(hostId, newhost);
             locations.put(descr.location(), newhost);
             return new HostEvent(HOST_ADDED, newhost);
@@ -169,12 +216,12 @@
             return new HostEvent(HOST_MOVED, host);
         }
 
-        if (host.ipAddresses().contains(descr.ipAddress())) {
+        if (host.ipAddresses().containsAll(descr.ipAddress())) {
             return null;
         }
 
         Set<IpPrefix> addresses = new HashSet<>(host.ipAddresses());
-        addresses.add(descr.ipAddress());
+        addresses.addAll(descr.ipAddress());
         StoredHost updated = new StoredHost(providerId, host.id(),
                                             host.mac(), host.vlan(),
                                             host.location, addresses);
@@ -381,6 +428,10 @@
         public HostLocation location() {
             return location.value();
         }
+
+        public Timestamp timestamp() {
+            return location.timestamp();
+        }
     }
 
     private void notifyPeers(InternalHostRemovedEvent event) throws IOException {
@@ -399,6 +450,16 @@
         clusterCommunicator.broadcast(message);
     }
 
+    private void unicastMessage(NodeId peer,
+                                MessageSubject subject,
+                                Object event) throws IOException {
+        ClusterMessage message = new ClusterMessage(
+                clusterService.getLocalNode().id(),
+                subject,
+                SERIALIZER.encode(event));
+        clusterCommunicator.unicast(message, peer);
+    }
+
     private void notifyDelegateIfNotNull(HostEvent event) {
         if (event != null) {
             notifyDelegate(event);
@@ -434,4 +495,165 @@
             notifyDelegateIfNotNull(removeHostInternal(hostId, timestamp));
         }
     }
+
+    private final class SendAdvertisementTask implements Runnable {
+
+        @Override
+        public void run() {
+            if (Thread.currentThread().isInterrupted()) {
+                log.info("Interrupted, quitting");
+                return;
+            }
+
+            try {
+                final NodeId self = clusterService.getLocalNode().id();
+                Set<ControllerNode> nodes = clusterService.getNodes();
+
+                ImmutableList<NodeId> nodeIds = FluentIterable.from(nodes)
+                        .transform(toNodeId())
+                        .toList();
+
+                if (nodeIds.size() == 1 && nodeIds.get(0).equals(self)) {
+                    log.debug("No other peers in the cluster.");
+                    return;
+                }
+
+                NodeId peer;
+                do {
+                    int idx = RandomUtils.nextInt(0, nodeIds.size());
+                    peer = nodeIds.get(idx);
+                } while (peer.equals(self));
+
+                HostAntiEntropyAdvertisement ad = createAdvertisement();
+
+                if (Thread.currentThread().isInterrupted()) {
+                    log.info("Interrupted, quitting");
+                    return;
+                }
+
+                try {
+                    unicastMessage(peer, GossipHostStoreMessageSubjects.HOST_ANTI_ENTROPY_ADVERTISEMENT, ad);
+                } catch (IOException e) {
+                    log.debug("Failed to send anti-entropy advertisement", e);
+                    return;
+                }
+            } catch (Exception e) {
+                // catch all Exception to avoid Scheduled task being suppressed.
+                log.error("Exception thrown while sending advertisement", e);
+            }
+        }
+    }
+
+    private HostAntiEntropyAdvertisement createAdvertisement() {
+        final NodeId self = clusterService.getLocalNode().id();
+
+        Map<HostFragmentId, Timestamp> timestamps = new HashMap<>(hosts.size());
+        Map<HostId, Timestamp> tombstones = new HashMap<>(removedHosts.size());
+
+        for (Entry<HostId, StoredHost> e : hosts.entrySet()) {
+
+            final HostId hostId = e.getKey();
+            final StoredHost hostInfo = e.getValue();
+            final ProviderId providerId = hostInfo.providerId();
+            timestamps.put(new HostFragmentId(hostId, providerId), hostInfo.timestamp());
+        }
+
+        for (Entry<HostId, Timestamped<Host>> e : removedHosts.entrySet()) {
+            tombstones.put(e.getKey(), e.getValue().timestamp());
+        }
+
+        return new HostAntiEntropyAdvertisement(self, timestamps, tombstones);
+    }
+
+    private synchronized void handleAntiEntropyAdvertisement(HostAntiEntropyAdvertisement ad) {
+
+        final NodeId sender = ad.sender();
+
+        for (Entry<HostId, StoredHost> host : hosts.entrySet()) {
+            // for each locally live Hosts...
+            final HostId hostId = host.getKey();
+            final StoredHost localHost = host.getValue();
+            final ProviderId providerId = localHost.providerId();
+            final HostFragmentId hostFragId = new HostFragmentId(hostId, providerId);
+            final Timestamp localLiveTimestamp = localHost.timestamp();
+
+            Timestamp remoteTimestamp = ad.timestamps().get(hostFragId);
+            if (remoteTimestamp == null) {
+                remoteTimestamp = ad.tombstones().get(hostId);
+            }
+            if (remoteTimestamp == null ||
+                localLiveTimestamp.compareTo(remoteTimestamp) > 0) {
+
+                // local is more recent, push
+                // TODO: annotation is lost
+                final HostDescription desc = new DefaultHostDescription(
+                            localHost.mac(),
+                            localHost.vlan(),
+                            localHost.location(),
+                            localHost.ipAddresses());
+                try {
+                    unicastMessage(sender, GossipHostStoreMessageSubjects.HOST_UPDATED,
+                            new InternalHostEvent(providerId, hostId, desc, localHost.timestamp()));
+                } catch (IOException e1) {
+                    log.debug("Failed to send advertisement response", e1);
+                }
+            }
+
+            final Timestamp remoteDeadTimestamp = ad.tombstones().get(hostId);
+            if (remoteDeadTimestamp != null &&
+                remoteDeadTimestamp.compareTo(localLiveTimestamp) > 0) {
+                // sender has recent remove
+                notifyDelegateIfNotNull(removeHostInternal(hostId, remoteDeadTimestamp));
+            }
+        }
+
+        for (Entry<HostId, Timestamped<Host>> dead : removedHosts.entrySet()) {
+            // for each locally dead Hosts
+            final HostId hostId = dead.getKey();
+            final Timestamp localDeadTimestamp = dead.getValue().timestamp();
+
+            // TODO: pick proper ProviderId, when supporting multi-provider
+            final ProviderId providerId = dead.getValue().value().providerId();
+            final HostFragmentId hostFragId = new HostFragmentId(hostId, providerId);
+
+            final Timestamp remoteLiveTimestamp = ad.timestamps().get(hostFragId);
+            if (remoteLiveTimestamp != null &&
+                localDeadTimestamp.compareTo(remoteLiveTimestamp) > 0) {
+                // sender has zombie, push
+                try {
+                    unicastMessage(sender, GossipHostStoreMessageSubjects.HOST_REMOVED,
+                            new InternalHostRemovedEvent(hostId, localDeadTimestamp));
+                } catch (IOException e1) {
+                    log.debug("Failed to send advertisement response", e1);
+                }
+            }
+        }
+
+
+        for (Entry<HostId, Timestamp> e : ad.tombstones().entrySet()) {
+            // for each remote tombstone advertisement...
+            final HostId hostId = e.getKey();
+            final Timestamp adRemoveTimestamp = e.getValue();
+
+            final StoredHost storedHost = hosts.get(hostId);
+            if (storedHost == null) {
+                continue;
+            }
+            if (adRemoveTimestamp.compareTo(storedHost.timestamp()) > 0) {
+                // sender has recent remove info, locally remove
+                notifyDelegateIfNotNull(removeHostInternal(hostId, adRemoveTimestamp));
+            }
+        }
+    }
+
+    private final class InternalHostAntiEntropyAdvertisementListener implements
+            ClusterMessageHandler {
+
+        @Override
+        public void handle(ClusterMessage message) {
+            log.debug("Received Host Anti-Entropy advertisement from peer: {}", message.sender());
+            HostAntiEntropyAdvertisement advertisement = SERIALIZER.decode(message.payload());
+            handleAntiEntropyAdvertisement(advertisement);
+        }
+    }
 }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStoreMessageSubjects.java b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStoreMessageSubjects.java
index 27cf4ce..0a9f0e0 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStoreMessageSubjects.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStoreMessageSubjects.java
@@ -4,6 +4,11 @@
 
 public final class GossipHostStoreMessageSubjects {
     private GossipHostStoreMessageSubjects() {}
-    public static final MessageSubject HOST_UPDATED = new MessageSubject("peer-host-updated");
-    public static final MessageSubject HOST_REMOVED = new MessageSubject("peer-host-removed");
+
+    public static final MessageSubject HOST_UPDATED
+        = new MessageSubject("peer-host-updated");
+    public static final MessageSubject HOST_REMOVED
+        = new MessageSubject("peer-host-removed");
+    public static final MessageSubject HOST_ANTI_ENTROPY_ADVERTISEMENT
+        = new MessageSubject("host-enti-entropy-advertisement");;
 }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/HostAntiEntropyAdvertisement.java b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/HostAntiEntropyAdvertisement.java
new file mode 100644
index 0000000..6139005
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/HostAntiEntropyAdvertisement.java
@@ -0,0 +1,48 @@
+package org.onlab.onos.store.host.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Map;
+
+import org.onlab.onos.cluster.NodeId;
+import org.onlab.onos.net.HostId;
+import org.onlab.onos.store.Timestamp;
+
+/**
+ * Host AE Advertisement message.
+ */
+public final class HostAntiEntropyAdvertisement {
+
+    private final NodeId sender;
+    private final Map<HostFragmentId, Timestamp> timestamps;
+    private final Map<HostId, Timestamp> tombstones;
+
+
+    public HostAntiEntropyAdvertisement(NodeId sender,
+                Map<HostFragmentId, Timestamp> timestamps,
+                Map<HostId, Timestamp> tombstones) {
+        this.sender = checkNotNull(sender);
+        this.timestamps = checkNotNull(timestamps);
+        this.tombstones = checkNotNull(tombstones);
+    }
+
+    public NodeId sender() {
+        return sender;
+    }
+
+    public Map<HostFragmentId, Timestamp> timestamps() {
+        return timestamps;
+    }
+
+    public Map<HostId, Timestamp> tombstones() {
+        return tombstones;
+    }
+
+    // For serializer
+    @SuppressWarnings("unused")
+    private HostAntiEntropyAdvertisement() {
+        this.sender = null;
+        this.timestamps = null;
+        this.tombstones = null;
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/HostFragmentId.java b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/HostFragmentId.java
new file mode 100644
index 0000000..34dbff6
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/HostFragmentId.java
@@ -0,0 +1,62 @@
+package org.onlab.onos.store.host.impl;
+
+import java.util.Objects;
+
+import org.onlab.onos.net.HostId;
+import org.onlab.onos.net.provider.ProviderId;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Identifier for HostDescription from a Provider.
+ */
+public final class HostFragmentId {
+    public final ProviderId providerId;
+    public final HostId hostId;
+
+    public HostFragmentId(HostId hostId, ProviderId providerId) {
+        this.providerId = providerId;
+        this.hostId = hostId;
+    }
+
+    public HostId hostId() {
+        return hostId;
+    }
+
+    public ProviderId providerId() {
+        return providerId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(providerId, hostId);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof HostFragmentId)) {
+            return false;
+        }
+        HostFragmentId that = (HostFragmentId) obj;
+        return Objects.equals(this.hostId, that.hostId) &&
+               Objects.equals(this.providerId, that.providerId);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("providerId", providerId)
+                .add("hostId", hostId)
+                .toString();
+    }
+
+    // for serializer
+    @SuppressWarnings("unused")
+    private HostFragmentId() {
+        this.providerId = null;
+        this.hostId = null;
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/package-info.java b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/package-info.java
new file mode 100644
index 0000000..72a4ce4
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Implementation of host store using distributed p2p synchronization protocol.
+ */
+package org.onlab.onos.store.host.impl;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java
index a6c1660..6e8a367 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java
@@ -1,7 +1,6 @@
 package org.onlab.onos.store.link.impl;
 
 import com.google.common.base.Function;
-import com.google.common.base.Predicate;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableList;
@@ -27,7 +26,6 @@
 import org.onlab.onos.net.SparseAnnotations;
 import org.onlab.onos.net.Link.Type;
 import org.onlab.onos.net.LinkKey;
-import org.onlab.onos.net.Provided;
 import org.onlab.onos.net.device.DeviceClockService;
 import org.onlab.onos.net.link.DefaultLinkDescription;
 import org.onlab.onos.net.link.LinkDescription;
@@ -70,7 +68,9 @@
 import static org.onlab.util.Tools.namedThreads;
 import static org.slf4j.LoggerFactory.getLogger;
 import static com.google.common.collect.Multimaps.synchronizedSetMultimap;
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Predicates.notNull;
+import static org.onlab.onos.store.link.impl.GossipLinkStoreMessageSubjects.LINK_ANTI_ENTROPY_ADVERTISEMENT;
 
 /**
  * Manages inventory of infrastructure links in distributed data store
@@ -239,9 +239,9 @@
         LinkKey key = linkKey(linkDescription.src(), linkDescription.dst());
         final LinkEvent event;
         final Timestamped<LinkDescription> mergedDesc;
-        synchronized (getLinkDescriptions(key)) {
+        synchronized (getOrCreateLinkDescriptions(key)) {
             event = createOrUpdateLinkInternal(providerId, deltaDesc);
-            mergedDesc = getLinkDescriptions(key).get(providerId);
+            mergedDesc = getOrCreateLinkDescriptions(key).get(providerId);
         }
 
         if (event != null) {
@@ -265,7 +265,7 @@
 
         LinkKey key = linkKey(linkDescription.value().src(),
                               linkDescription.value().dst());
-        Map<ProviderId, Timestamped<LinkDescription>> descs = getLinkDescriptions(key);
+        Map<ProviderId, Timestamped<LinkDescription>> descs = getOrCreateLinkDescriptions(key);
 
         synchronized (descs) {
             // if the link was previously removed, we should proceed if and
@@ -296,7 +296,7 @@
             ProviderId providerId,
             Timestamped<LinkDescription> linkDescription) {
 
-        // merge existing attributes and merge
+        // merge existing annotations
         Timestamped<LinkDescription> existingLinkDescription = descs.get(providerId);
         if (existingLinkDescription != null && existingLinkDescription.isNewer(linkDescription)) {
             return null;
@@ -377,14 +377,54 @@
         return event;
     }
 
-    private LinkEvent removeLinkInternal(LinkKey key, Timestamp timestamp) {
-        Map<ProviderId, Timestamped<LinkDescription>> linkDescriptions =
-                getLinkDescriptions(key);
+    private static Timestamped<LinkDescription> getPrimaryDescription(
+                Map<ProviderId, Timestamped<LinkDescription>> linkDescriptions) {
+
         synchronized (linkDescriptions) {
+            for (Entry<ProviderId, Timestamped<LinkDescription>>
+                    e : linkDescriptions.entrySet()) {
+
+                if (!e.getKey().isAncillary()) {
+                    return e.getValue();
+                }
+            }
+        }
+        return null;
+    }
+
+
+    // TODO: consider slicing out as Timestamp utils
+    /**
+     * Checks is timestamp is more recent than timestamped object.
+     *
+     * @param timestamp to check if this is more recent then other
+     * @param timestamped object to be tested against
+     * @return true if {@code timestamp} is more recent than {@code timestamped}
+     *         or {@code timestamped is null}
+     */
+    private static boolean isMoreRecent(Timestamp timestamp, Timestamped<?> timestamped) {
+        checkNotNull(timestamp);
+        if (timestamped == null) {
+            return true;
+        }
+        return timestamp.compareTo(timestamped.timestamp()) > 0;
+    }
+
+    private LinkEvent removeLinkInternal(LinkKey key, Timestamp timestamp) {
+        Map<ProviderId, Timestamped<LinkDescription>> linkDescriptions
+            = getOrCreateLinkDescriptions(key);
+
+        synchronized (linkDescriptions) {
+            if (linkDescriptions.isEmpty()) {
+                // never seen such link before. keeping timestamp for record
+                removedLinks.put(key, timestamp);
+                return null;
+            }
             // accept removal request if given timestamp is newer than
             // the latest Timestamp from Primary provider
-            ProviderId primaryProviderId = pickPrimaryProviderId(linkDescriptions);
-            if (linkDescriptions.get(primaryProviderId).isNewer(timestamp)) {
+            Timestamped<LinkDescription> prim = getPrimaryDescription(linkDescriptions);
+            if (!isMoreRecent(timestamp, prim)) {
+                // outdated remove request, ignore
                 return null;
             }
             removedLinks.put(key, timestamp);
@@ -406,12 +446,13 @@
     /**
      * @return primary ProviderID, or randomly chosen one if none exists
      */
-    private ProviderId pickPrimaryProviderId(
+    private static ProviderId pickBaseProviderId(
             Map<ProviderId, Timestamped<LinkDescription>> linkDescriptions) {
 
         ProviderId fallBackPrimary = null;
         for (Entry<ProviderId, Timestamped<LinkDescription>> e : linkDescriptions.entrySet()) {
             if (!e.getKey().isAncillary()) {
+                // found primary
                 return e.getKey();
             } else if (fallBackPrimary == null) {
                 // pick randomly as a fallback in case there is no primary
@@ -421,9 +462,10 @@
         return fallBackPrimary;
     }
 
+    // Guarded by linkDescs value (=locking each Link)
     private Link composeLink(Map<ProviderId, Timestamped<LinkDescription>> descs) {
-        ProviderId primaryProviderId = pickPrimaryProviderId(descs);
-        Timestamped<LinkDescription> base = descs.get(primaryProviderId);
+        ProviderId baseProviderId = pickBaseProviderId(descs);
+        Timestamped<LinkDescription> base = descs.get(baseProviderId);
 
         ConnectPoint src = base.value().src();
         ConnectPoint dst = base.value().dst();
@@ -432,7 +474,7 @@
         annotations = merge(annotations, base.value().annotations());
 
         for (Entry<ProviderId, Timestamped<LinkDescription>> e : descs.entrySet()) {
-            if (primaryProviderId.equals(e.getKey())) {
+            if (baseProviderId.equals(e.getKey())) {
                 continue;
             }
 
@@ -445,10 +487,10 @@
             annotations = merge(annotations, e.getValue().value().annotations());
         }
 
-        return new DefaultLink(primaryProviderId , src, dst, type, annotations);
+        return new DefaultLink(baseProviderId, src, dst, type, annotations);
     }
 
-    private Map<ProviderId, Timestamped<LinkDescription>> getLinkDescriptions(LinkKey key) {
+    private Map<ProviderId, Timestamped<LinkDescription>> getOrCreateLinkDescriptions(LinkKey key) {
         Map<ProviderId, Timestamped<LinkDescription>> r;
         r = linkDescs.get(key);
         if (r != null) {
@@ -464,11 +506,11 @@
         }
     }
 
-    private Timestamped<LinkDescription> getLinkDescription(LinkKey key, ProviderId providerId) {
-        return getLinkDescriptions(key).get(providerId);
-    }
-
     private final Function<LinkKey, Link> lookupLink = new LookupLink();
+    /**
+     * Returns a Function to lookup Link instance using LinkKey from cache.
+     * @return
+     */
     private Function<LinkKey, Link> lookupLink() {
         return lookupLink;
     }
@@ -476,20 +518,11 @@
     private final class LookupLink implements Function<LinkKey, Link> {
         @Override
         public Link apply(LinkKey input) {
-            return links.get(input);
-        }
-    }
-
-    private static final class IsPrimary implements Predicate<Provided> {
-
-        private static final Predicate<Provided> IS_PRIMARY = new IsPrimary();
-        public static final Predicate<Provided> isPrimary() {
-            return IS_PRIMARY;
-        }
-
-        @Override
-        public boolean apply(Provided input) {
-            return !input.providerId().isAncillary();
+            if (input == null) {
+                return null;
+            } else {
+                return links.get(input);
+            }
         }
     }
 
@@ -499,7 +532,6 @@
         }
     }
 
-    // TODO: should we be throwing exception?
     private void broadcastMessage(MessageSubject subject, Object event) throws IOException {
         ClusterMessage message = new ClusterMessage(
                 clusterService.getLocalNode().id(),
@@ -508,17 +540,12 @@
         clusterCommunicator.broadcast(message);
     }
 
-    // TODO: should we be throwing exception?
-    private void unicastMessage(NodeId recipient, MessageSubject subject, Object event) {
-        try {
-            ClusterMessage message = new ClusterMessage(
-                    clusterService.getLocalNode().id(),
-                    subject,
-                    SERIALIZER.encode(event));
-            clusterCommunicator.unicast(message, recipient);
-        } catch (IOException e) {
-            log.error("Failed to send a {} message to {}", subject.value(), recipient);
-        }
+    private void unicastMessage(NodeId recipient, MessageSubject subject, Object event) throws IOException {
+        ClusterMessage message = new ClusterMessage(
+                clusterService.getLocalNode().id(),
+                subject,
+                SERIALIZER.encode(event));
+        clusterCommunicator.unicast(message, recipient);
     }
 
     private void notifyPeers(InternalLinkEvent event) throws IOException {
@@ -529,12 +556,22 @@
         broadcastMessage(GossipLinkStoreMessageSubjects.LINK_REMOVED, event);
     }
 
+    // notify peer, silently ignoring error
     private void notifyPeer(NodeId peer, InternalLinkEvent event) {
-        unicastMessage(peer, GossipLinkStoreMessageSubjects.LINK_UPDATE, event);
+        try {
+            unicastMessage(peer, GossipLinkStoreMessageSubjects.LINK_UPDATE, event);
+        } catch (IOException e) {
+            log.debug("Failed to notify peer {} with message {}", peer, event);
+        }
     }
 
+    // notify peer, silently ignoring error
     private void notifyPeer(NodeId peer, InternalLinkRemovedEvent event) {
-        unicastMessage(peer, GossipLinkStoreMessageSubjects.LINK_REMOVED, event);
+        try {
+            unicastMessage(peer, GossipLinkStoreMessageSubjects.LINK_REMOVED, event);
+        } catch (IOException e) {
+            log.debug("Failed to notify peer {} with message {}", peer, event);
+        }
     }
 
     private final class SendAdvertisementTask implements Runnable {
@@ -573,9 +610,9 @@
                 }
 
                 try {
-                    unicastMessage(peer, GossipLinkStoreMessageSubjects.LINK_ANTI_ENTROPY_ADVERTISEMENT, ad);
-                } catch (Exception e) {
-                    log.error("Failed to send anti-entropy advertisement", e);
+                    unicastMessage(peer, LINK_ANTI_ENTROPY_ADVERTISEMENT, ad);
+                } catch (IOException e) {
+                    log.debug("Failed to send anti-entropy advertisement to {}", peer);
                     return;
                 }
             } catch (Exception e) {
@@ -608,42 +645,75 @@
         return new LinkAntiEntropyAdvertisement(self, linkTimestamps, linkTombstones);
     }
 
-    private void handleAntiEntropyAdvertisement(LinkAntiEntropyAdvertisement advertisement) {
+    private void handleAntiEntropyAdvertisement(LinkAntiEntropyAdvertisement ad) {
 
-        NodeId peer = advertisement.sender();
+        final NodeId sender = ad.sender();
+        boolean localOutdated = false;
 
-        Map<LinkFragmentId, Timestamp> linkTimestamps = advertisement.linkTimestamps();
-        Map<LinkKey, Timestamp> linkTombstones = advertisement.linkTombstones();
-        for (Map.Entry<LinkFragmentId, Timestamp> entry : linkTimestamps.entrySet()) {
-            LinkFragmentId linkFragmentId = entry.getKey();
-            Timestamp peerTimestamp = entry.getValue();
+        for (Entry<LinkKey, Map<ProviderId, Timestamped<LinkDescription>>>
+                l : linkDescs.entrySet()) {
 
-            LinkKey key = linkFragmentId.linkKey();
-            ProviderId providerId = linkFragmentId.providerId();
+            final LinkKey key = l.getKey();
+            final Map<ProviderId, Timestamped<LinkDescription>> link = l.getValue();
+            synchronized (link) {
+                Timestamp localLatest = removedLinks.get(key);
 
-            Timestamped<LinkDescription> linkDescription = getLinkDescription(key, providerId);
-            if (linkDescription.isNewer(peerTimestamp)) {
-                // I have more recent link description. update peer.
-                notifyPeer(peer, new InternalLinkEvent(providerId, linkDescription));
-            }
-            // else TODO: Peer has more recent link description. request it.
+                for (Entry<ProviderId, Timestamped<LinkDescription>> p : link.entrySet()) {
+                    final ProviderId providerId = p.getKey();
+                    final Timestamped<LinkDescription> pDesc = p.getValue();
 
-            Timestamp linkRemovedTimestamp = removedLinks.get(key);
-            if (linkRemovedTimestamp != null && linkRemovedTimestamp.compareTo(peerTimestamp) > 0) {
-                // peer has a zombie link. update peer.
-                notifyPeer(peer, new InternalLinkRemovedEvent(key, linkRemovedTimestamp));
+                    final LinkFragmentId fragId = new LinkFragmentId(key, providerId);
+                    // remote
+                    Timestamp remoteTimestamp = ad.linkTimestamps().get(fragId);
+                    if (remoteTimestamp == null) {
+                        remoteTimestamp = ad.linkTombstones().get(key);
+                    }
+                    if (remoteTimestamp == null ||
+                        pDesc.isNewer(remoteTimestamp)) {
+                        // I have more recent link description. update peer.
+                        notifyPeer(sender, new InternalLinkEvent(providerId, pDesc));
+                    } else {
+                        final Timestamp remoteLive = ad.linkTimestamps().get(fragId);
+                        if (remoteLive != null &&
+                            remoteLive.compareTo(pDesc.timestamp()) > 0) {
+                            // I have something outdated
+                            localOutdated = true;
+                        }
+                    }
+
+                    // search local latest along the way
+                    if (localLatest == null ||
+                        pDesc.isNewer(localLatest)) {
+                        localLatest = pDesc.timestamp();
+                    }
+                }
+                // Tests if remote remove is more recent then local latest.
+                final Timestamp remoteRemove = ad.linkTombstones().get(key);
+                if (remoteRemove != null) {
+                    if (localLatest != null &&
+                        localLatest.compareTo(remoteRemove) < 0) {
+                        // remote remove is more recent
+                        notifyDelegateIfNotNull(removeLinkInternal(key, remoteRemove));
+                    }
+                }
             }
         }
 
-        for (Map.Entry<LinkKey, Timestamp> entry : linkTombstones.entrySet()) {
-            LinkKey key = entry.getKey();
-            Timestamp peerTimestamp = entry.getValue();
+        // populate remove info if not known locally
+        for (Entry<LinkKey, Timestamp> remoteRm : ad.linkTombstones().entrySet()) {
+            final LinkKey key = remoteRm.getKey();
+            final Timestamp remoteRemove = remoteRm.getValue();
+            // relying on removeLinkInternal to ignore stale info
+            notifyDelegateIfNotNull(removeLinkInternal(key, remoteRemove));
+        }
 
-            ProviderId primaryProviderId = pickPrimaryProviderId(getLinkDescriptions(key));
-            if (primaryProviderId != null) {
-                if (!getLinkDescription(key, primaryProviderId).isNewer(peerTimestamp)) {
-                    notifyDelegateIfNotNull(removeLinkInternal(key, peerTimestamp));
-                }
+        if (localOutdated) {
+            // send back advertisement to speed up convergence
+            try {
+                unicastMessage(sender, LINK_ANTI_ENTROPY_ADVERTISEMENT,
+                                createAdvertisement());
+            } catch (IOException e) {
+                log.debug("Failed to send back active advertisement");
             }
         }
     }
diff --git a/core/store/hz/net/pom.xml b/core/store/hz/net/pom.xml
deleted file mode 100644
index 177e99e..0000000
--- a/core/store/hz/net/pom.xml
+++ /dev/null
@@ -1,55 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-
-    <parent>
-        <groupId>org.onlab.onos</groupId>
-        <artifactId>onos-core-hz</artifactId>
-        <version>1.0.0-SNAPSHOT</version>
-        <relativePath>../pom.xml</relativePath>
-    </parent>
-
-    <artifactId>onos-core-hz-net</artifactId>
-    <packaging>bundle</packaging>
-
-    <description>ONOS Hazelcast based distributed store subsystems</description>
-
-    <dependencies>
-        <dependency>
-            <groupId>org.onlab.onos</groupId>
-            <artifactId>onos-api</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.onlab.onos</groupId>
-            <artifactId>onos-core-hz-common</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onlab.onos</groupId>
-            <artifactId>onos-core-hz-common</artifactId>
-            <classifier>tests</classifier>
-            <scope>test</scope>
-            <version>${project.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.felix</groupId>
-            <artifactId>org.apache.felix.scr.annotations</artifactId>
-        </dependency>
-        <dependency>
-          <groupId>com.hazelcast</groupId>
-          <artifactId>hazelcast</artifactId>
-        </dependency>
-    </dependencies>
-
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.felix</groupId>
-                <artifactId>maven-scr-plugin</artifactId>
-            </plugin>
-        </plugins>
-    </build>
-
-</project>
diff --git a/core/store/hz/net/src/main/java/org/onlab/onos/store/device/impl/DistributedDeviceStore.java b/core/store/hz/net/src/main/java/org/onlab/onos/store/device/impl/DistributedDeviceStore.java
deleted file mode 100644
index 0016939..0000000
--- a/core/store/hz/net/src/main/java/org/onlab/onos/store/device/impl/DistributedDeviceStore.java
+++ /dev/null
@@ -1,408 +0,0 @@
-package org.onlab.onos.store.device.impl;
-
-import static com.google.common.base.Predicates.notNull;
-
-import com.google.common.base.Optional;
-import com.google.common.cache.LoadingCache;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSet.Builder;
-import com.hazelcast.core.IMap;
-import com.hazelcast.core.ISet;
-
-import org.apache.felix.scr.annotations.Activate;
-import org.apache.felix.scr.annotations.Component;
-import org.apache.felix.scr.annotations.Deactivate;
-import org.apache.felix.scr.annotations.Service;
-import org.onlab.onos.net.DefaultDevice;
-import org.onlab.onos.net.DefaultPort;
-import org.onlab.onos.net.Device;
-import org.onlab.onos.net.DeviceId;
-import org.onlab.onos.net.Port;
-import org.onlab.onos.net.PortNumber;
-import org.onlab.onos.net.device.DeviceDescription;
-import org.onlab.onos.net.device.DeviceEvent;
-import org.onlab.onos.net.device.DeviceStore;
-import org.onlab.onos.net.device.DeviceStoreDelegate;
-import org.onlab.onos.net.device.PortDescription;
-import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.common.AbsentInvalidatingLoadingCache;
-import org.onlab.onos.store.common.AbstractHazelcastStore;
-import org.onlab.onos.store.common.OptionalCacheLoader;
-import org.slf4j.Logger;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.cache.CacheBuilder.newBuilder;
-import static org.onlab.onos.net.device.DeviceEvent.Type.*;
-import static org.slf4j.LoggerFactory.getLogger;
-
-//TODO: Add support for multiple provider and annotations
-/**
- * Manages inventory of infrastructure devices using Hazelcast-backed map.
- */
-@Component(immediate = true)
-@Service
-public class DistributedDeviceStore
-        extends AbstractHazelcastStore<DeviceEvent, DeviceStoreDelegate>
-        implements DeviceStore {
-
-    private final Logger log = getLogger(getClass());
-
-    public static final String DEVICE_NOT_FOUND = "Device with ID %s not found";
-
-    // private IMap<DeviceId, DefaultDevice> cache;
-    private IMap<byte[], byte[]> rawDevices;
-    private LoadingCache<DeviceId, Optional<DefaultDevice>> devices;
-
-    // private ISet<DeviceId> availableDevices;
-    private ISet<byte[]> availableDevices;
-
-    // TODO DevicePorts is very inefficient consider restructuring.
-    // private IMap<DeviceId, Map<PortNumber, Port>> devicePorts;
-    private IMap<byte[], byte[]> rawDevicePorts;
-    private LoadingCache<DeviceId, Optional<Map<PortNumber, Port>>> devicePorts;
-
-    private String devicesListener;
-
-    private String portsListener;
-
-    @Override
-    @Activate
-    public void activate() {
-        super.activate();
-
-        // IMap event handler needs value
-        final boolean includeValue = true;
-
-        // TODO decide on Map name scheme to avoid collision
-        rawDevices = theInstance.getMap("devices");
-        final OptionalCacheLoader<DeviceId, DefaultDevice> deviceLoader
-                = new OptionalCacheLoader<>(serializer, rawDevices);
-        devices = new AbsentInvalidatingLoadingCache<>(newBuilder().build(deviceLoader));
-        // refresh/populate cache based on notification from other instance
-        devicesListener = rawDevices.addEntryListener(new RemoteDeviceEventHandler(devices), includeValue);
-
-        // TODO cache availableDevices
-        availableDevices = theInstance.getSet("availableDevices");
-
-        rawDevicePorts = theInstance.getMap("devicePorts");
-        final OptionalCacheLoader<DeviceId, Map<PortNumber, Port>> devicePortLoader
-                = new OptionalCacheLoader<>(serializer, rawDevicePorts);
-        devicePorts = new AbsentInvalidatingLoadingCache<>(newBuilder().build(devicePortLoader));
-        // refresh/populate cache based on notification from other instance
-        portsListener = rawDevicePorts.addEntryListener(new RemotePortEventHandler(devicePorts), includeValue);
-
-        loadDeviceCache();
-        loadDevicePortsCache();
-
-        log.info("Started");
-    }
-
-    @Deactivate
-    public void deactivate() {
-        rawDevicePorts.removeEntryListener(portsListener);
-        rawDevices.removeEntryListener(devicesListener);
-        log.info("Stopped");
-    }
-
-    @Override
-    public int getDeviceCount() {
-        return devices.asMap().size();
-    }
-
-    @Override
-    public Iterable<Device> getDevices() {
-        // TODO builder v.s. copyOf. Guava semms to be using copyOf?
-        Builder<Device> builder = ImmutableSet.builder();
-        for (Optional<DefaultDevice> e : devices.asMap().values()) {
-            if (e.isPresent()) {
-                builder.add(e.get());
-            }
-        }
-        return builder.build();
-    }
-
-    private void loadDeviceCache() {
-        for (byte[] keyBytes : rawDevices.keySet()) {
-            final DeviceId id = deserialize(keyBytes);
-            devices.refresh(id);
-        }
-    }
-
-    private void loadDevicePortsCache() {
-        for (byte[] keyBytes : rawDevicePorts.keySet()) {
-            final DeviceId id = deserialize(keyBytes);
-            devicePorts.refresh(id);
-        }
-    }
-
-    @Override
-    public Device getDevice(DeviceId deviceId) {
-        // TODO revisit if ignoring exception is safe.
-        return devices.getUnchecked(deviceId).orNull();
-    }
-
-    @Override
-    public DeviceEvent createOrUpdateDevice(ProviderId providerId, DeviceId deviceId,
-                                            DeviceDescription deviceDescription) {
-        DefaultDevice device = devices.getUnchecked(deviceId).orNull();
-        if (device == null) {
-            return createDevice(providerId, deviceId, deviceDescription);
-        }
-        return updateDevice(providerId, device, deviceDescription);
-    }
-
-    // Creates the device and returns the appropriate event if necessary.
-    private DeviceEvent createDevice(ProviderId providerId, DeviceId deviceId,
-                                     DeviceDescription desc) {
-        DefaultDevice device = new DefaultDevice(providerId, deviceId, desc.type(),
-                                                 desc.manufacturer(),
-                                                 desc.hwVersion(), desc.swVersion(),
-                                                 desc.serialNumber());
-
-        synchronized (this) {
-            final byte[] deviceIdBytes = serialize(deviceId);
-            rawDevices.put(deviceIdBytes, serialize(device));
-            devices.put(deviceId, Optional.of(device));
-
-            availableDevices.add(deviceIdBytes);
-        }
-        return new DeviceEvent(DEVICE_ADDED, device, null);
-    }
-
-    // Updates the device and returns the appropriate event if necessary.
-    private DeviceEvent updateDevice(ProviderId providerId, DefaultDevice device,
-                                     DeviceDescription desc) {
-        // We allow only certain attributes to trigger update
-        if (!Objects.equals(device.hwVersion(), desc.hwVersion()) ||
-                !Objects.equals(device.swVersion(), desc.swVersion())) {
-
-            DefaultDevice updated = new DefaultDevice(providerId, device.id(),
-                                                      desc.type(),
-                                                      desc.manufacturer(),
-                                                      desc.hwVersion(),
-                                                      desc.swVersion(),
-                                                      desc.serialNumber());
-            synchronized (this) {
-                final byte[] deviceIdBytes = serialize(device.id());
-                rawDevices.put(deviceIdBytes, serialize(updated));
-                devices.put(device.id(), Optional.of(updated));
-                availableDevices.add(serialize(device.id()));
-            }
-            return new DeviceEvent(DeviceEvent.Type.DEVICE_UPDATED, updated, null);
-        }
-
-        // Otherwise merely attempt to change availability
-        synchronized (this) {
-            boolean added = availableDevices.add(serialize(device.id()));
-            return !added ? null :
-                    new DeviceEvent(DEVICE_AVAILABILITY_CHANGED, device, null);
-        }
-    }
-
-    @Override
-    public DeviceEvent markOffline(DeviceId deviceId) {
-        synchronized (this) {
-            Device device = devices.getUnchecked(deviceId).orNull();
-            boolean removed = device != null && availableDevices.remove(serialize(deviceId));
-            return !removed ? null :
-                    new DeviceEvent(DEVICE_AVAILABILITY_CHANGED, device, null);
-        }
-    }
-
-    @Override
-    public List<DeviceEvent> updatePorts(ProviderId providerId, DeviceId deviceId,
-                                         List<PortDescription> portDescriptions) {
-        List<DeviceEvent> events = new ArrayList<>();
-        synchronized (this) {
-            Device device = devices.getUnchecked(deviceId).orNull();
-            checkArgument(device != null, DEVICE_NOT_FOUND, deviceId);
-            Map<PortNumber, Port> ports = getPortMap(deviceId);
-
-            // Add new ports
-            Set<PortNumber> processed = new HashSet<>();
-            for (PortDescription portDescription : portDescriptions) {
-                Port port = ports.get(portDescription.portNumber());
-                events.add(port == null ?
-                                   createPort(device, portDescription, ports) :
-                                   updatePort(device, port, portDescription, ports));
-                processed.add(portDescription.portNumber());
-            }
-
-            updatePortMap(deviceId, ports);
-
-            events.addAll(pruneOldPorts(device, ports, processed));
-        }
-        return FluentIterable.from(events).filter(notNull()).toList();
-    }
-
-    // Creates a new port based on the port description adds it to the map and
-    // Returns corresponding event.
-    //@GuardedBy("this")
-    private DeviceEvent createPort(Device device, PortDescription portDescription,
-                                   Map<PortNumber, Port> ports) {
-        DefaultPort port = new DefaultPort(device, portDescription.portNumber(),
-                                           portDescription.isEnabled());
-        ports.put(port.number(), port);
-        updatePortMap(device.id(), ports);
-        return new DeviceEvent(PORT_ADDED, device, port);
-    }
-
-    // Checks if the specified port requires update and if so, it replaces the
-    // existing entry in the map and returns corresponding event.
-    //@GuardedBy("this")
-    private DeviceEvent updatePort(Device device, Port port,
-                                   PortDescription portDescription,
-                                   Map<PortNumber, Port> ports) {
-        if (port.isEnabled() != portDescription.isEnabled()) {
-            DefaultPort updatedPort =
-                    new DefaultPort(device, portDescription.portNumber(),
-                                    portDescription.isEnabled());
-            ports.put(port.number(), updatedPort);
-            updatePortMap(device.id(), ports);
-            return new DeviceEvent(PORT_UPDATED, device, updatedPort);
-        }
-        return null;
-    }
-
-    // Prunes the specified list of ports based on which ports are in the
-    // processed list and returns list of corresponding events.
-    //@GuardedBy("this")
-    private List<DeviceEvent> pruneOldPorts(Device device,
-                                            Map<PortNumber, Port> ports,
-                                            Set<PortNumber> processed) {
-        List<DeviceEvent> events = new ArrayList<>();
-        Iterator<PortNumber> iterator = ports.keySet().iterator();
-        while (iterator.hasNext()) {
-            PortNumber portNumber = iterator.next();
-            if (!processed.contains(portNumber)) {
-                events.add(new DeviceEvent(PORT_REMOVED, device,
-                                           ports.get(portNumber)));
-                iterator.remove();
-            }
-        }
-        if (!events.isEmpty()) {
-            updatePortMap(device.id(), ports);
-        }
-        return events;
-    }
-
-    // Gets the map of ports for the specified device; if one does not already
-    // exist, it creates and registers a new one.
-    // WARN: returned value is a copy, changes made to the Map
-    //       needs to be written back using updatePortMap
-    //@GuardedBy("this")
-    private Map<PortNumber, Port> getPortMap(DeviceId deviceId) {
-        Map<PortNumber, Port> ports = devicePorts.getUnchecked(deviceId).orNull();
-        if (ports == null) {
-            ports = new HashMap<>();
-            // this probably is waste of time in most cases.
-            updatePortMap(deviceId, ports);
-        }
-        return ports;
-    }
-
-    //@GuardedBy("this")
-    private void updatePortMap(DeviceId deviceId, Map<PortNumber, Port> ports) {
-        rawDevicePorts.put(serialize(deviceId), serialize(ports));
-        devicePorts.put(deviceId, Optional.of(ports));
-    }
-
-    @Override
-    public DeviceEvent updatePortStatus(ProviderId providerId, DeviceId deviceId,
-                                        PortDescription portDescription) {
-        synchronized (this) {
-            Device device = devices.getUnchecked(deviceId).orNull();
-            checkArgument(device != null, DEVICE_NOT_FOUND, deviceId);
-            Map<PortNumber, Port> ports = getPortMap(deviceId);
-            Port port = ports.get(portDescription.portNumber());
-            return updatePort(device, port, portDescription, ports);
-        }
-    }
-
-    @Override
-    public List<Port> getPorts(DeviceId deviceId) {
-        Map<PortNumber, Port> ports = devicePorts.getUnchecked(deviceId).orNull();
-        return ports == null ? Collections.<Port>emptyList() : ImmutableList.copyOf(ports.values());
-    }
-
-    @Override
-    public Port getPort(DeviceId deviceId, PortNumber portNumber) {
-        Map<PortNumber, Port> ports = devicePorts.getUnchecked(deviceId).orNull();
-        return ports == null ? null : ports.get(portNumber);
-    }
-
-    @Override
-    public boolean isAvailable(DeviceId deviceId) {
-        return availableDevices.contains(serialize(deviceId));
-    }
-
-    @Override
-    public DeviceEvent removeDevice(DeviceId deviceId) {
-        synchronized (this) {
-            byte[] deviceIdBytes = serialize(deviceId);
-
-            // TODO conditional remove?
-            Device device = deserialize(rawDevices.remove(deviceIdBytes));
-            devices.invalidate(deviceId);
-            return device == null ? null :
-                    new DeviceEvent(DEVICE_REMOVED, device, null);
-        }
-    }
-
-    private class RemoteDeviceEventHandler extends RemoteCacheEventHandler<DeviceId, DefaultDevice> {
-        public RemoteDeviceEventHandler(LoadingCache<DeviceId, Optional<DefaultDevice>> cache) {
-            super(cache);
-        }
-
-        @Override
-        protected void onAdd(DeviceId deviceId, DefaultDevice device) {
-            notifyDelegate(new DeviceEvent(DEVICE_ADDED, device));
-        }
-
-        @Override
-        protected void onRemove(DeviceId deviceId, DefaultDevice device) {
-            notifyDelegate(new DeviceEvent(DEVICE_REMOVED, device));
-        }
-
-        @Override
-        protected void onUpdate(DeviceId deviceId, DefaultDevice oldDevice, DefaultDevice device) {
-            notifyDelegate(new DeviceEvent(DEVICE_UPDATED, device));
-        }
-    }
-
-    private class RemotePortEventHandler extends RemoteCacheEventHandler<DeviceId, Map<PortNumber, Port>> {
-        public RemotePortEventHandler(LoadingCache<DeviceId, Optional<Map<PortNumber, Port>>> cache) {
-            super(cache);
-        }
-
-        @Override
-        protected void onAdd(DeviceId deviceId, Map<PortNumber, Port> ports) {
-//            notifyDelegate(new DeviceEvent(PORT_ADDED, getDevice(deviceId)));
-        }
-
-        @Override
-        protected void onRemove(DeviceId deviceId, Map<PortNumber, Port> ports) {
-//            notifyDelegate(new DeviceEvent(PORT_REMOVED, getDevice(deviceId)));
-        }
-
-        @Override
-        protected void onUpdate(DeviceId deviceId, Map<PortNumber, Port> oldPorts, Map<PortNumber, Port> ports) {
-//            notifyDelegate(new DeviceEvent(PORT_UPDATED, getDevice(deviceId)));
-        }
-    }
-
-
-    // TODO cache serialized DeviceID if we suffer from serialization cost
-}
diff --git a/core/store/hz/net/src/main/java/org/onlab/onos/store/device/impl/NoOpClockProviderService.java b/core/store/hz/net/src/main/java/org/onlab/onos/store/device/impl/NoOpClockProviderService.java
deleted file mode 100644
index 4626fa4..0000000
--- a/core/store/hz/net/src/main/java/org/onlab/onos/store/device/impl/NoOpClockProviderService.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.onlab.onos.store.device.impl;
-
-import org.apache.felix.scr.annotations.Component;
-import org.apache.felix.scr.annotations.Service;
-import org.onlab.onos.mastership.MastershipTerm;
-import org.onlab.onos.net.DeviceId;
-import org.onlab.onos.net.device.DeviceClockProviderService;
-
-// FIXME: Code clone in onos-core-trivial, onos-core-hz-net
-/**
- * Dummy implementation of {@link DeviceClockProviderService}.
- */
-@Component(immediate = true)
-@Service
-public class NoOpClockProviderService implements DeviceClockProviderService {
-
-    @Override
-    public void setMastershipTerm(DeviceId deviceId, MastershipTerm term) {
-    }
-}
diff --git a/core/store/hz/net/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java b/core/store/hz/net/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
deleted file mode 100644
index 084435f..0000000
--- a/core/store/hz/net/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
+++ /dev/null
@@ -1,142 +0,0 @@
-package org.onlab.onos.store.flow.impl;
-
-import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_REMOVED;
-import static org.slf4j.LoggerFactory.getLogger;
-
-import java.util.Collection;
-import java.util.Collections;
-
-import org.apache.felix.scr.annotations.Activate;
-import org.apache.felix.scr.annotations.Component;
-import org.apache.felix.scr.annotations.Deactivate;
-import org.apache.felix.scr.annotations.Service;
-import org.onlab.onos.ApplicationId;
-import org.onlab.onos.net.DeviceId;
-import org.onlab.onos.net.flow.DefaultFlowEntry;
-import org.onlab.onos.net.flow.FlowEntry;
-import org.onlab.onos.net.flow.FlowEntry.FlowEntryState;
-import org.onlab.onos.net.flow.FlowRule;
-import org.onlab.onos.net.flow.FlowRuleEvent;
-import org.onlab.onos.net.flow.FlowRuleEvent.Type;
-import org.onlab.onos.net.flow.FlowRuleStore;
-import org.onlab.onos.net.flow.FlowRuleStoreDelegate;
-import org.onlab.onos.store.AbstractStore;
-import org.slf4j.Logger;
-
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Multimap;
-
-/**
- * Manages inventory of flow rules using trivial in-memory implementation.
- */
-//FIXME I LIE. I AIN'T DISTRIBUTED
-@Component(immediate = true)
-@Service
-public class DistributedFlowRuleStore
-        extends AbstractStore<FlowRuleEvent, FlowRuleStoreDelegate>
-        implements FlowRuleStore {
-
-    private final Logger log = getLogger(getClass());
-
-    // store entries as a pile of rules, no info about device tables
-    private final Multimap<DeviceId, FlowEntry> flowEntries =
-            ArrayListMultimap.<DeviceId, FlowEntry>create();
-
-    private final Multimap<Short, FlowRule> flowEntriesById =
-            ArrayListMultimap.<Short, FlowRule>create();
-
-    @Activate
-    public void activate() {
-        log.info("Started");
-    }
-
-    @Deactivate
-    public void deactivate() {
-        log.info("Stopped");
-    }
-
-
-    @Override
-    public int getFlowRuleCount() {
-        return flowEntries.size();
-    }
-
-    @Override
-    public synchronized FlowEntry getFlowEntry(FlowRule rule) {
-        for (FlowEntry f : flowEntries.get(rule.deviceId())) {
-            if (f.equals(rule)) {
-                return f;
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public synchronized Iterable<FlowEntry> getFlowEntries(DeviceId deviceId) {
-        Collection<FlowEntry> rules = flowEntries.get(deviceId);
-        if (rules == null) {
-            return Collections.emptyList();
-        }
-        return ImmutableSet.copyOf(rules);
-    }
-
-    @Override
-    public synchronized Iterable<FlowRule> getFlowRulesByAppId(ApplicationId appId) {
-        Collection<FlowRule> rules = flowEntriesById.get(appId.id());
-        if (rules == null) {
-            return Collections.emptyList();
-        }
-        return ImmutableSet.copyOf(rules);
-    }
-
-    @Override
-    public synchronized void storeFlowRule(FlowRule rule) {
-        FlowEntry f = new DefaultFlowEntry(rule);
-        DeviceId did = f.deviceId();
-        if (!flowEntries.containsEntry(did, f)) {
-            flowEntries.put(did, f);
-            flowEntriesById.put(rule.appId(), f);
-        }
-    }
-
-    @Override
-    public synchronized void deleteFlowRule(FlowRule rule) {
-        FlowEntry entry = getFlowEntry(rule);
-        if (entry == null) {
-            return;
-        }
-        entry.setState(FlowEntryState.PENDING_REMOVE);
-    }
-
-    @Override
-    public synchronized FlowRuleEvent addOrUpdateFlowRule(FlowEntry rule) {
-        DeviceId did = rule.deviceId();
-
-        // check if this new rule is an update to an existing entry
-        FlowEntry stored = getFlowEntry(rule);
-        if (stored != null) {
-            stored.setBytes(rule.bytes());
-            stored.setLife(rule.life());
-            stored.setPackets(rule.packets());
-            if (stored.state() == FlowEntryState.PENDING_ADD) {
-                stored.setState(FlowEntryState.ADDED);
-                return new FlowRuleEvent(Type.RULE_ADDED, rule);
-            }
-            return new FlowRuleEvent(Type.RULE_UPDATED, rule);
-        }
-
-        flowEntries.put(did, rule);
-        return null;
-    }
-
-    @Override
-    public synchronized FlowRuleEvent removeFlowRule(FlowEntry rule) {
-        // This is where one could mark a rule as removed and still keep it in the store.
-        if (flowEntries.remove(rule.deviceId(), rule)) {
-            return new FlowRuleEvent(RULE_REMOVED, rule);
-        } else {
-            return null;
-        }
-    }
-}
diff --git a/core/store/hz/net/src/main/java/org/onlab/onos/store/host/impl/DistributedHostStore.java b/core/store/hz/net/src/main/java/org/onlab/onos/store/host/impl/DistributedHostStore.java
deleted file mode 100644
index 0ca4ae2..0000000
--- a/core/store/hz/net/src/main/java/org/onlab/onos/store/host/impl/DistributedHostStore.java
+++ /dev/null
@@ -1,302 +0,0 @@
-package org.onlab.onos.store.host.impl;
-
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
-import org.apache.felix.scr.annotations.Activate;
-import org.apache.felix.scr.annotations.Component;
-import org.apache.felix.scr.annotations.Deactivate;
-import org.apache.felix.scr.annotations.Service;
-import org.onlab.onos.net.Annotations;
-import org.onlab.onos.net.ConnectPoint;
-import org.onlab.onos.net.DefaultHost;
-import org.onlab.onos.net.DeviceId;
-import org.onlab.onos.net.Host;
-import org.onlab.onos.net.HostId;
-import org.onlab.onos.net.HostLocation;
-import org.onlab.onos.net.host.HostDescription;
-import org.onlab.onos.net.host.HostEvent;
-import org.onlab.onos.net.host.HostStore;
-import org.onlab.onos.net.host.HostStoreDelegate;
-import org.onlab.onos.net.host.PortAddresses;
-import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.AbstractStore;
-import org.onlab.packet.IpPrefix;
-import org.onlab.packet.MacAddress;
-import org.onlab.packet.VlanId;
-import org.slf4j.Logger;
-
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import static org.onlab.onos.net.host.HostEvent.Type.*;
-import static org.slf4j.LoggerFactory.getLogger;
-
-/**
- * TEMPORARY: Manages inventory of end-station hosts using distributed
- * structures implementation.
- */
-//FIXME: I LIE I AM NOT DISTRIBUTED
-@Component(immediate = true)
-@Service
-public class DistributedHostStore
-        extends AbstractStore<HostEvent, HostStoreDelegate>
-        implements HostStore {
-
-    private final Logger log = getLogger(getClass());
-
-    // Host inventory
-    private final Map<HostId, StoredHost> hosts = new ConcurrentHashMap<>(2000000, 0.75f, 16);
-
-    // Hosts tracked by their location
-    private final Multimap<ConnectPoint, Host> locations = HashMultimap.create();
-
-    private final Map<ConnectPoint, PortAddresses> portAddresses =
-            new ConcurrentHashMap<>();
-
-    @Activate
-    public void activate() {
-        log.info("Started");
-    }
-
-    @Deactivate
-    public void deactivate() {
-        log.info("Stopped");
-    }
-
-    @Override
-    public HostEvent createOrUpdateHost(ProviderId providerId, HostId hostId,
-                                        HostDescription hostDescription) {
-        StoredHost host = hosts.get(hostId);
-        if (host == null) {
-            return createHost(providerId, hostId, hostDescription);
-        }
-        return updateHost(providerId, host, hostDescription);
-    }
-
-    // creates a new host and sends HOST_ADDED
-    private HostEvent createHost(ProviderId providerId, HostId hostId,
-                                 HostDescription descr) {
-        StoredHost newhost = new StoredHost(providerId, hostId,
-                                            descr.hwAddress(),
-                                            descr.vlan(),
-                                            descr.location(),
-                                            ImmutableSet.of(descr.ipAddress()));
-        synchronized (this) {
-            hosts.put(hostId, newhost);
-            locations.put(descr.location(), newhost);
-        }
-        return new HostEvent(HOST_ADDED, newhost);
-    }
-
-    // checks for type of update to host, sends appropriate event
-    private HostEvent updateHost(ProviderId providerId, StoredHost host,
-                                 HostDescription descr) {
-        HostEvent event;
-        if (!host.location().equals(descr.location())) {
-            host.setLocation(descr.location());
-            return new HostEvent(HOST_MOVED, host);
-        }
-
-        if (host.ipAddresses().contains(descr.ipAddress())) {
-            return null;
-        }
-
-        Set<IpPrefix> addresses = new HashSet<>(host.ipAddresses());
-        addresses.add(descr.ipAddress());
-        StoredHost updated = new StoredHost(providerId, host.id(),
-                                            host.mac(), host.vlan(),
-                                            descr.location(), addresses);
-        event = new HostEvent(HOST_UPDATED, updated);
-        synchronized (this) {
-            hosts.put(host.id(), updated);
-            locations.remove(host.location(), host);
-            locations.put(updated.location(), updated);
-        }
-        return event;
-    }
-
-    @Override
-    public HostEvent removeHost(HostId hostId) {
-        synchronized (this) {
-            Host host = hosts.remove(hostId);
-            if (host != null) {
-                locations.remove((host.location()), host);
-                return new HostEvent(HOST_REMOVED, host);
-            }
-            return null;
-        }
-    }
-
-    @Override
-    public int getHostCount() {
-        return hosts.size();
-    }
-
-    @Override
-    public Iterable<Host> getHosts() {
-        return ImmutableSet.<Host>copyOf(hosts.values());
-    }
-
-    @Override
-    public Host getHost(HostId hostId) {
-        return hosts.get(hostId);
-    }
-
-    @Override
-    public Set<Host> getHosts(VlanId vlanId) {
-        Set<Host> vlanset = new HashSet<>();
-        for (Host h : hosts.values()) {
-            if (h.vlan().equals(vlanId)) {
-                vlanset.add(h);
-            }
-        }
-        return vlanset;
-    }
-
-    @Override
-    public Set<Host> getHosts(MacAddress mac) {
-        Set<Host> macset = new HashSet<>();
-        for (Host h : hosts.values()) {
-            if (h.mac().equals(mac)) {
-                macset.add(h);
-            }
-        }
-        return macset;
-    }
-
-    @Override
-    public Set<Host> getHosts(IpPrefix ip) {
-        Set<Host> ipset = new HashSet<>();
-        for (Host h : hosts.values()) {
-            if (h.ipAddresses().contains(ip)) {
-                ipset.add(h);
-            }
-        }
-        return ipset;
-    }
-
-    @Override
-    public Set<Host> getConnectedHosts(ConnectPoint connectPoint) {
-        return ImmutableSet.copyOf(locations.get(connectPoint));
-    }
-
-    @Override
-    public Set<Host> getConnectedHosts(DeviceId deviceId) {
-        Set<Host> hostset = new HashSet<>();
-        for (ConnectPoint p : locations.keySet()) {
-            if (p.deviceId().equals(deviceId)) {
-                hostset.addAll(locations.get(p));
-            }
-        }
-        return hostset;
-    }
-
-    @Override
-    public void updateAddressBindings(PortAddresses addresses) {
-        synchronized (portAddresses) {
-            PortAddresses existing = portAddresses.get(addresses.connectPoint());
-            if (existing == null) {
-                portAddresses.put(addresses.connectPoint(), addresses);
-            } else {
-                Set<IpPrefix> union = Sets.union(existing.ips(), addresses.ips())
-                        .immutableCopy();
-
-                MacAddress newMac = (addresses.mac() == null) ? existing.mac()
-                        : addresses.mac();
-
-                PortAddresses newAddresses =
-                        new PortAddresses(addresses.connectPoint(), union, newMac);
-
-                portAddresses.put(newAddresses.connectPoint(), newAddresses);
-            }
-        }
-    }
-
-    @Override
-    public void removeAddressBindings(PortAddresses addresses) {
-        synchronized (portAddresses) {
-            PortAddresses existing = portAddresses.get(addresses.connectPoint());
-            if (existing != null) {
-                Set<IpPrefix> difference =
-                        Sets.difference(existing.ips(), addresses.ips()).immutableCopy();
-
-                // If they removed the existing mac, set the new mac to null.
-                // Otherwise, keep the existing mac.
-                MacAddress newMac = existing.mac();
-                if (addresses.mac() != null && addresses.mac().equals(existing.mac())) {
-                    newMac = null;
-                }
-
-                PortAddresses newAddresses =
-                        new PortAddresses(addresses.connectPoint(), difference, newMac);
-
-                portAddresses.put(newAddresses.connectPoint(), newAddresses);
-            }
-        }
-    }
-
-    @Override
-    public void clearAddressBindings(ConnectPoint connectPoint) {
-        synchronized (portAddresses) {
-            portAddresses.remove(connectPoint);
-        }
-    }
-
-    @Override
-    public Set<PortAddresses> getAddressBindings() {
-        synchronized (portAddresses) {
-            return new HashSet<>(portAddresses.values());
-        }
-    }
-
-    @Override
-    public PortAddresses getAddressBindingsForPort(ConnectPoint connectPoint) {
-        PortAddresses addresses;
-
-        synchronized (portAddresses) {
-            addresses = portAddresses.get(connectPoint);
-        }
-
-        if (addresses == null) {
-            addresses = new PortAddresses(connectPoint, null, null);
-        }
-
-        return addresses;
-    }
-
-    // Auxiliary extension to allow location to mutate.
-    private class StoredHost extends DefaultHost {
-        private HostLocation location;
-
-        /**
-         * Creates an end-station host using the supplied information.
-         *
-         * @param providerId  provider identity
-         * @param id          host identifier
-         * @param mac         host MAC address
-         * @param vlan        host VLAN identifier
-         * @param location    host location
-         * @param ips         host IP addresses
-         * @param annotations optional key/value annotations
-         */
-        public StoredHost(ProviderId providerId, HostId id,
-                          MacAddress mac, VlanId vlan, HostLocation location,
-                          Set<IpPrefix> ips, Annotations... annotations) {
-            super(providerId, id, mac, vlan, location, ips, annotations);
-            this.location = location;
-        }
-
-        void setLocation(HostLocation location) {
-            this.location = location;
-        }
-
-        @Override
-        public HostLocation location() {
-            return location;
-        }
-    }
-}
diff --git a/core/store/hz/net/src/main/java/org/onlab/onos/store/link/impl/DistributedLinkStore.java b/core/store/hz/net/src/main/java/org/onlab/onos/store/link/impl/DistributedLinkStore.java
deleted file mode 100644
index 90ae6fe..0000000
--- a/core/store/hz/net/src/main/java/org/onlab/onos/store/link/impl/DistributedLinkStore.java
+++ /dev/null
@@ -1,263 +0,0 @@
-package org.onlab.onos.store.link.impl;
-
-import static com.google.common.cache.CacheBuilder.newBuilder;
-import static org.onlab.onos.net.Link.Type.DIRECT;
-import static org.onlab.onos.net.Link.Type.INDIRECT;
-import static org.onlab.onos.net.LinkKey.linkKey;
-import static org.onlab.onos.net.link.LinkEvent.Type.LINK_ADDED;
-import static org.onlab.onos.net.link.LinkEvent.Type.LINK_REMOVED;
-import static org.onlab.onos.net.link.LinkEvent.Type.LINK_UPDATED;
-import static org.slf4j.LoggerFactory.getLogger;
-
-import java.util.HashSet;
-import java.util.Set;
-
-import org.apache.felix.scr.annotations.Activate;
-import org.apache.felix.scr.annotations.Component;
-import org.apache.felix.scr.annotations.Deactivate;
-import org.apache.felix.scr.annotations.Service;
-import org.onlab.onos.net.ConnectPoint;
-import org.onlab.onos.net.DefaultLink;
-import org.onlab.onos.net.DeviceId;
-import org.onlab.onos.net.Link;
-import org.onlab.onos.net.LinkKey;
-import org.onlab.onos.net.link.LinkDescription;
-import org.onlab.onos.net.link.LinkEvent;
-import org.onlab.onos.net.link.LinkStore;
-import org.onlab.onos.net.link.LinkStoreDelegate;
-import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.common.AbsentInvalidatingLoadingCache;
-import org.onlab.onos.store.common.AbstractHazelcastStore;
-import org.onlab.onos.store.common.OptionalCacheLoader;
-import org.slf4j.Logger;
-
-import com.google.common.base.Optional;
-import com.google.common.cache.LoadingCache;
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.ImmutableSet.Builder;
-import com.hazelcast.core.IMap;
-
-//TODO: Add support for multiple provider and annotations
-/**
- * Manages inventory of infrastructure links using Hazelcast-backed map.
- */
-@Component(immediate = true)
-@Service
-public class DistributedLinkStore
-    extends AbstractHazelcastStore<LinkEvent, LinkStoreDelegate>
-    implements LinkStore {
-
-    private final Logger log = getLogger(getClass());
-
-    // Link inventory
-    private IMap<byte[], byte[]> rawLinks;
-    private LoadingCache<LinkKey, Optional<DefaultLink>> links;
-
-    // TODO synchronize?
-    // Egress and ingress link sets
-    private final Multimap<DeviceId, Link> srcLinks = HashMultimap.create();
-    private final Multimap<DeviceId, Link> dstLinks = HashMultimap.create();
-
-    private String linksListener;
-
-    @Override
-    @Activate
-    public void activate() {
-        super.activate();
-
-        boolean includeValue = true;
-
-        // TODO decide on Map name scheme to avoid collision
-        rawLinks = theInstance.getMap("links");
-        final OptionalCacheLoader<LinkKey, DefaultLink> linkLoader
-                = new OptionalCacheLoader<>(serializer, rawLinks);
-        links = new AbsentInvalidatingLoadingCache<>(newBuilder().build(linkLoader));
-        // refresh/populate cache based on notification from other instance
-        linksListener = rawLinks.addEntryListener(new RemoteLinkEventHandler(links), includeValue);
-
-        loadLinkCache();
-
-        log.info("Started");
-    }
-
-    @Deactivate
-    public void deactivate() {
-        rawLinks.removeEntryListener(linksListener);
-        log.info("Stopped");
-    }
-
-    private void loadLinkCache() {
-        for (byte[] keyBytes : rawLinks.keySet()) {
-            final LinkKey id = deserialize(keyBytes);
-            links.refresh(id);
-        }
-    }
-
-    @Override
-    public int getLinkCount() {
-        return links.asMap().size();
-    }
-
-    @Override
-    public Iterable<Link> getLinks() {
-        Builder<Link> builder = ImmutableSet.builder();
-        for (Optional<DefaultLink> e : links.asMap().values()) {
-            if (e.isPresent()) {
-                builder.add(e.get());
-            }
-        }
-        return builder.build();
-    }
-
-    @Override
-    public Set<Link> getDeviceEgressLinks(DeviceId deviceId) {
-        return ImmutableSet.copyOf(srcLinks.get(deviceId));
-    }
-
-    @Override
-    public Set<Link> getDeviceIngressLinks(DeviceId deviceId) {
-        return ImmutableSet.copyOf(dstLinks.get(deviceId));
-    }
-
-    @Override
-    public Link getLink(ConnectPoint src, ConnectPoint dst) {
-        return links.getUnchecked(linkKey(src, dst)).orNull();
-    }
-
-    @Override
-    public Set<Link> getEgressLinks(ConnectPoint src) {
-        Set<Link> egress = new HashSet<>();
-        for (Link link : srcLinks.get(src.deviceId())) {
-            if (link.src().equals(src)) {
-                egress.add(link);
-            }
-        }
-        return egress;
-    }
-
-    @Override
-    public Set<Link> getIngressLinks(ConnectPoint dst) {
-        Set<Link> ingress = new HashSet<>();
-        for (Link link : dstLinks.get(dst.deviceId())) {
-            if (link.dst().equals(dst)) {
-                ingress.add(link);
-            }
-        }
-        return ingress;
-    }
-
-    @Override
-    public LinkEvent createOrUpdateLink(ProviderId providerId,
-                                        LinkDescription linkDescription) {
-        LinkKey key = linkKey(linkDescription.src(), linkDescription.dst());
-        Optional<DefaultLink> link = links.getUnchecked(key);
-        if (!link.isPresent()) {
-            return createLink(providerId, key, linkDescription);
-        }
-        return updateLink(providerId, link.get(), key, linkDescription);
-    }
-
-    // Creates and stores the link and returns the appropriate event.
-    private LinkEvent createLink(ProviderId providerId, LinkKey key,
-                                 LinkDescription linkDescription) {
-        DefaultLink link = new DefaultLink(providerId, key.src(), key.dst(),
-                                           linkDescription.type());
-        synchronized (this) {
-            final byte[] keyBytes = serialize(key);
-            rawLinks.put(keyBytes, serialize(link));
-            links.asMap().putIfAbsent(key, Optional.of(link));
-
-            addNewLink(link);
-        }
-        return new LinkEvent(LINK_ADDED, link);
-    }
-
-    // update Egress and ingress link sets
-    private void addNewLink(DefaultLink link) {
-        synchronized (this) {
-            srcLinks.put(link.src().deviceId(), link);
-            dstLinks.put(link.dst().deviceId(), link);
-        }
-    }
-
-    // Updates, if necessary the specified link and returns the appropriate event.
-    private LinkEvent updateLink(ProviderId providerId, DefaultLink link,
-                                 LinkKey key, LinkDescription linkDescription) {
-        // FIXME confirm Link update condition is OK
-        if (link.type() == INDIRECT && linkDescription.type() == DIRECT) {
-            synchronized (this) {
-
-                DefaultLink updated =
-                        new DefaultLink(providerId, link.src(), link.dst(),
-                                        linkDescription.type());
-                final byte[] keyBytes = serialize(key);
-                rawLinks.put(keyBytes, serialize(updated));
-                links.asMap().replace(key, Optional.of(link), Optional.of(updated));
-
-                replaceLink(link, updated);
-                return new LinkEvent(LINK_UPDATED, updated);
-            }
-        }
-        return null;
-    }
-
-    // update Egress and ingress link sets
-    private void replaceLink(DefaultLink link, DefaultLink updated) {
-        synchronized (this) {
-            srcLinks.remove(link.src().deviceId(), link);
-            dstLinks.remove(link.dst().deviceId(), link);
-
-            srcLinks.put(link.src().deviceId(), updated);
-            dstLinks.put(link.dst().deviceId(), updated);
-        }
-    }
-
-    @Override
-    public LinkEvent removeLink(ConnectPoint src, ConnectPoint dst) {
-        synchronized (this) {
-            LinkKey key = linkKey(src, dst);
-            byte[] keyBytes = serialize(key);
-            Link link = deserialize(rawLinks.remove(keyBytes));
-            links.invalidate(key);
-            if (link != null) {
-                removeLink(link);
-                return new LinkEvent(LINK_REMOVED, link);
-            }
-            return null;
-        }
-    }
-
-    // update Egress and ingress link sets
-    private void removeLink(Link link) {
-        synchronized (this) {
-            srcLinks.remove(link.src().deviceId(), link);
-            dstLinks.remove(link.dst().deviceId(), link);
-        }
-    }
-
-    private class RemoteLinkEventHandler extends RemoteCacheEventHandler<LinkKey, DefaultLink> {
-        public RemoteLinkEventHandler(LoadingCache<LinkKey, Optional<DefaultLink>> cache) {
-            super(cache);
-        }
-
-        @Override
-        protected void onAdd(LinkKey key, DefaultLink newVal) {
-            addNewLink(newVal);
-            notifyDelegate(new LinkEvent(LINK_ADDED, newVal));
-        }
-
-        @Override
-        protected void onUpdate(LinkKey key, DefaultLink oldVal, DefaultLink newVal) {
-            replaceLink(oldVal, newVal);
-            notifyDelegate(new LinkEvent(LINK_UPDATED, newVal));
-        }
-
-        @Override
-        protected void onRemove(LinkKey key, DefaultLink val) {
-            removeLink(val);
-            notifyDelegate(new LinkEvent(LINK_REMOVED, val));
-        }
-    }
-}
diff --git a/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/DefaultTopology.java b/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/DefaultTopology.java
deleted file mode 100644
index 5574d27..0000000
--- a/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/DefaultTopology.java
+++ /dev/null
@@ -1,444 +0,0 @@
-package org.onlab.onos.store.topology.impl;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSetMultimap;
-import org.onlab.graph.DijkstraGraphSearch;
-import org.onlab.graph.GraphPathSearch;
-import org.onlab.graph.TarjanGraphSearch;
-import org.onlab.onos.net.AbstractModel;
-import org.onlab.onos.net.ConnectPoint;
-import org.onlab.onos.net.DefaultPath;
-import org.onlab.onos.net.DeviceId;
-import org.onlab.onos.net.Link;
-import org.onlab.onos.net.Path;
-import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.net.topology.ClusterId;
-import org.onlab.onos.net.topology.DefaultTopologyCluster;
-import org.onlab.onos.net.topology.DefaultTopologyVertex;
-import org.onlab.onos.net.topology.GraphDescription;
-import org.onlab.onos.net.topology.LinkWeight;
-import org.onlab.onos.net.topology.Topology;
-import org.onlab.onos.net.topology.TopologyCluster;
-import org.onlab.onos.net.topology.TopologyEdge;
-import org.onlab.onos.net.topology.TopologyGraph;
-import org.onlab.onos.net.topology.TopologyVertex;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import static com.google.common.base.MoreObjects.toStringHelper;
-import static com.google.common.collect.ImmutableSetMultimap.Builder;
-import static org.onlab.graph.GraphPathSearch.Result;
-import static org.onlab.graph.TarjanGraphSearch.SCCResult;
-import static org.onlab.onos.net.Link.Type.INDIRECT;
-
-/**
- * Default implementation of the topology descriptor. This carries the
- * backing topology data.
- */
-public class DefaultTopology extends AbstractModel implements Topology {
-
-    private static final DijkstraGraphSearch<TopologyVertex, TopologyEdge> DIJKSTRA =
-            new DijkstraGraphSearch<>();
-    private static final TarjanGraphSearch<TopologyVertex, TopologyEdge> TARJAN =
-            new TarjanGraphSearch<>();
-
-    private static final ProviderId PID = new ProviderId("core", "org.onlab.onos.net");
-
-    private final long time;
-    private final TopologyGraph graph;
-
-    private final SCCResult<TopologyVertex, TopologyEdge> clusterResults;
-    private final ImmutableMap<DeviceId, Result<TopologyVertex, TopologyEdge>> results;
-    private final ImmutableSetMultimap<PathKey, Path> paths;
-
-    private final ImmutableMap<ClusterId, TopologyCluster> clusters;
-    private final ImmutableSet<ConnectPoint> infrastructurePoints;
-    private final ImmutableSetMultimap<ClusterId, ConnectPoint> broadcastSets;
-
-    private ImmutableMap<DeviceId, TopologyCluster> clustersByDevice;
-    private ImmutableSetMultimap<TopologyCluster, DeviceId> devicesByCluster;
-    private ImmutableSetMultimap<TopologyCluster, Link> linksByCluster;
-
-
-    /**
-     * Creates a topology descriptor attributed to the specified provider.
-     *
-     * @param providerId  identity of the provider
-     * @param description data describing the new topology
-     */
-    DefaultTopology(ProviderId providerId, GraphDescription description) {
-        super(providerId);
-        this.time = description.timestamp();
-
-        // Build the graph
-        this.graph = new DefaultTopologyGraph(description.vertexes(),
-                                              description.edges());
-
-        this.results = searchForShortestPaths();
-        this.paths = buildPaths();
-
-        this.clusterResults = searchForClusters();
-        this.clusters = buildTopologyClusters();
-
-        buildIndexes();
-
-        this.broadcastSets = buildBroadcastSets();
-        this.infrastructurePoints = findInfrastructurePoints();
-    }
-
-    @Override
-    public long time() {
-        return time;
-    }
-
-    @Override
-    public int clusterCount() {
-        return clusters.size();
-    }
-
-    @Override
-    public int deviceCount() {
-        return graph.getVertexes().size();
-    }
-
-    @Override
-    public int linkCount() {
-        return graph.getEdges().size();
-    }
-
-    @Override
-    public int pathCount() {
-        return paths.size();
-    }
-
-    /**
-     * Returns the backing topology graph.
-     *
-     * @return topology graph
-     */
-    TopologyGraph getGraph() {
-        return graph;
-    }
-
-    /**
-     * Returns the set of topology clusters.
-     *
-     * @return set of clusters
-     */
-    Set<TopologyCluster> getClusters() {
-        return ImmutableSet.copyOf(clusters.values());
-    }
-
-    /**
-     * Returns the specified topology cluster.
-     *
-     * @param clusterId cluster identifier
-     * @return topology cluster
-     */
-    TopologyCluster getCluster(ClusterId clusterId) {
-        return clusters.get(clusterId);
-    }
-
-    /**
-     * Returns the topology cluster that contains the given device.
-     *
-     * @param deviceId device identifier
-     * @return topology cluster
-     */
-    TopologyCluster getCluster(DeviceId deviceId) {
-        return clustersByDevice.get(deviceId);
-    }
-
-    /**
-     * Returns the set of cluster devices.
-     *
-     * @param cluster topology cluster
-     * @return cluster devices
-     */
-    Set<DeviceId> getClusterDevices(TopologyCluster cluster) {
-        return devicesByCluster.get(cluster);
-    }
-
-    /**
-     * Returns the set of cluster links.
-     *
-     * @param cluster topology cluster
-     * @return cluster links
-     */
-    Set<Link> getClusterLinks(TopologyCluster cluster) {
-        return linksByCluster.get(cluster);
-    }
-
-    /**
-     * Indicates whether the given point is an infrastructure link end-point.
-     *
-     * @param connectPoint connection point
-     * @return true if infrastructure
-     */
-    boolean isInfrastructure(ConnectPoint connectPoint) {
-        return infrastructurePoints.contains(connectPoint);
-    }
-
-    /**
-     * Indicates whether the given point is part of a broadcast set.
-     *
-     * @param connectPoint connection point
-     * @return true if in broadcast set
-     */
-    boolean isBroadcastPoint(ConnectPoint connectPoint) {
-        // Any non-infrastructure, i.e. edge points are assumed to be OK.
-        if (!isInfrastructure(connectPoint)) {
-            return true;
-        }
-
-        // Find the cluster to which the device belongs.
-        TopologyCluster cluster = clustersByDevice.get(connectPoint.deviceId());
-        if (cluster == null) {
-            throw new IllegalArgumentException("No cluster found for device " + connectPoint.deviceId());
-        }
-
-        // If the broadcast set is null or empty, or if the point explicitly
-        // belongs to it, return true;
-        Set<ConnectPoint> points = broadcastSets.get(cluster.id());
-        return points == null || points.isEmpty() || points.contains(connectPoint);
-    }
-
-    /**
-     * Returns the size of the cluster broadcast set.
-     *
-     * @param clusterId cluster identifier
-     * @return size of the cluster broadcast set
-     */
-    int broadcastSetSize(ClusterId clusterId) {
-        return broadcastSets.get(clusterId).size();
-    }
-
-    /**
-     * Returns the set of pre-computed shortest paths between source and
-     * destination devices.
-     *
-     * @param src source device
-     * @param dst destination device
-     * @return set of shortest paths
-     */
-    Set<Path> getPaths(DeviceId src, DeviceId dst) {
-        return paths.get(new PathKey(src, dst));
-    }
-
-    /**
-     * Computes on-demand the set of shortest paths between source and
-     * destination devices.
-     *
-     * @param src source device
-     * @param dst destination device
-     * @return set of shortest paths
-     */
-    Set<Path> getPaths(DeviceId src, DeviceId dst, LinkWeight weight) {
-        GraphPathSearch.Result<TopologyVertex, TopologyEdge> result =
-                DIJKSTRA.search(graph, new DefaultTopologyVertex(src),
-                                new DefaultTopologyVertex(dst), weight);
-        ImmutableSet.Builder<Path> builder = ImmutableSet.builder();
-        for (org.onlab.graph.Path<TopologyVertex, TopologyEdge> path : result.paths()) {
-            builder.add(networkPath(path));
-        }
-        return builder.build();
-    }
-
-
-    // Searches the graph for all shortest paths and returns the search results.
-    private ImmutableMap<DeviceId, Result<TopologyVertex, TopologyEdge>> searchForShortestPaths() {
-        ImmutableMap.Builder<DeviceId, Result<TopologyVertex, TopologyEdge>> builder = ImmutableMap.builder();
-
-        // Search graph paths for each source to all destinations.
-        LinkWeight weight = new HopCountLinkWeight(graph.getVertexes().size());
-        for (TopologyVertex src : graph.getVertexes()) {
-            builder.put(src.deviceId(), DIJKSTRA.search(graph, src, null, weight));
-        }
-        return builder.build();
-    }
-
-    // Builds network paths from the graph path search results
-    private ImmutableSetMultimap<PathKey, Path> buildPaths() {
-        Builder<PathKey, Path> builder = ImmutableSetMultimap.builder();
-        for (DeviceId deviceId : results.keySet()) {
-            Result<TopologyVertex, TopologyEdge> result = results.get(deviceId);
-            for (org.onlab.graph.Path<TopologyVertex, TopologyEdge> path : result.paths()) {
-                builder.put(new PathKey(path.src().deviceId(), path.dst().deviceId()),
-                            networkPath(path));
-            }
-        }
-        return builder.build();
-    }
-
-    // Converts graph path to a network path with the same cost.
-    private Path networkPath(org.onlab.graph.Path<TopologyVertex, TopologyEdge> path) {
-        List<Link> links = new ArrayList<>();
-        for (TopologyEdge edge : path.edges()) {
-            links.add(edge.link());
-        }
-        return new DefaultPath(PID, links, path.cost());
-    }
-
-
-    // Searches for SCC clusters in the network topology graph using Tarjan
-    // algorithm.
-    private SCCResult<TopologyVertex, TopologyEdge> searchForClusters() {
-        return TARJAN.search(graph, new NoIndirectLinksWeight());
-    }
-
-    // Builds the topology clusters and returns the id-cluster bindings.
-    private ImmutableMap<ClusterId, TopologyCluster> buildTopologyClusters() {
-        ImmutableMap.Builder<ClusterId, TopologyCluster> clusterBuilder = ImmutableMap.builder();
-        SCCResult<TopologyVertex, TopologyEdge> result =
-                TARJAN.search(graph, new NoIndirectLinksWeight());
-
-        // Extract both vertexes and edges from the results; the lists form
-        // pairs along the same index.
-        List<Set<TopologyVertex>> clusterVertexes = result.clusterVertexes();
-        List<Set<TopologyEdge>> clusterEdges = result.clusterEdges();
-
-        // Scan over the lists and create a cluster from the results.
-        for (int i = 0, n = result.clusterCount(); i < n; i++) {
-            Set<TopologyVertex> vertexSet = clusterVertexes.get(i);
-            Set<TopologyEdge> edgeSet = clusterEdges.get(i);
-
-            ClusterId cid = ClusterId.clusterId(i);
-            DefaultTopologyCluster cluster =
-                    new DefaultTopologyCluster(cid, vertexSet.size(), edgeSet.size(),
-                                               findRoot(vertexSet).deviceId());
-            clusterBuilder.put(cid, cluster);
-        }
-        return clusterBuilder.build();
-    }
-
-    // Finds the vertex whose device id is the lexicographical minimum in the
-    // specified set.
-    private TopologyVertex findRoot(Set<TopologyVertex> vertexSet) {
-        TopologyVertex minVertex = null;
-        for (TopologyVertex vertex : vertexSet) {
-            if (minVertex == null ||
-                    minVertex.deviceId().toString()
-                            .compareTo(minVertex.deviceId().toString()) < 0) {
-                minVertex = vertex;
-            }
-        }
-        return minVertex;
-    }
-
-    // Processes a map of broadcast sets for each cluster.
-    private ImmutableSetMultimap<ClusterId, ConnectPoint> buildBroadcastSets() {
-        Builder<ClusterId, ConnectPoint> builder = ImmutableSetMultimap.builder();
-        for (TopologyCluster cluster : clusters.values()) {
-            addClusterBroadcastSet(cluster, builder);
-        }
-        return builder.build();
-    }
-
-    // Finds all broadcast points for the cluster. These are those connection
-    // points which lie along the shortest paths between the cluster root and
-    // all other devices within the cluster.
-    private void addClusterBroadcastSet(TopologyCluster cluster,
-                                        Builder<ClusterId, ConnectPoint> builder) {
-        // Use the graph root search results to build the broadcast set.
-        Result<TopologyVertex, TopologyEdge> result = results.get(cluster.root());
-        for (Map.Entry<TopologyVertex, Set<TopologyEdge>> entry : result.parents().entrySet()) {
-            TopologyVertex vertex = entry.getKey();
-
-            // Ignore any parents that lead outside the cluster.
-            if (clustersByDevice.get(vertex.deviceId()) != cluster) {
-                continue;
-            }
-
-            // Ignore any back-link sets that are empty.
-            Set<TopologyEdge> parents = entry.getValue();
-            if (parents.isEmpty()) {
-                continue;
-            }
-
-            // Use the first back-link source and destinations to add to the
-            // broadcast set.
-            Link link = parents.iterator().next().link();
-            builder.put(cluster.id(), link.src());
-            builder.put(cluster.id(), link.dst());
-        }
-    }
-
-    // Collects and returns an set of all infrastructure link end-points.
-    private ImmutableSet<ConnectPoint> findInfrastructurePoints() {
-        ImmutableSet.Builder<ConnectPoint> builder = ImmutableSet.builder();
-        for (TopologyEdge edge : graph.getEdges()) {
-            builder.add(edge.link().src());
-            builder.add(edge.link().dst());
-        }
-        return builder.build();
-    }
-
-    // Builds cluster-devices, cluster-links and device-cluster indexes.
-    private void buildIndexes() {
-        // Prepare the index builders
-        ImmutableMap.Builder<DeviceId, TopologyCluster> clusterBuilder = ImmutableMap.builder();
-        ImmutableSetMultimap.Builder<TopologyCluster, DeviceId> devicesBuilder = ImmutableSetMultimap.builder();
-        ImmutableSetMultimap.Builder<TopologyCluster, Link> linksBuilder = ImmutableSetMultimap.builder();
-
-        // Now scan through all the clusters
-        for (TopologyCluster cluster : clusters.values()) {
-            int i = cluster.id().index();
-
-            // Scan through all the cluster vertexes.
-            for (TopologyVertex vertex : clusterResults.clusterVertexes().get(i)) {
-                devicesBuilder.put(cluster, vertex.deviceId());
-                clusterBuilder.put(vertex.deviceId(), cluster);
-            }
-
-            // Scan through all the cluster edges.
-            for (TopologyEdge edge : clusterResults.clusterEdges().get(i)) {
-                linksBuilder.put(cluster, edge.link());
-            }
-        }
-
-        // Finalize all indexes.
-        clustersByDevice = clusterBuilder.build();
-        devicesByCluster = devicesBuilder.build();
-        linksByCluster = linksBuilder.build();
-    }
-
-    // Link weight for measuring link cost as hop count with indirect links
-    // being as expensive as traversing the entire graph to assume the worst.
-    private static class HopCountLinkWeight implements LinkWeight {
-        private final int indirectLinkCost;
-
-        HopCountLinkWeight(int indirectLinkCost) {
-            this.indirectLinkCost = indirectLinkCost;
-        }
-
-        @Override
-        public double weight(TopologyEdge edge) {
-            // To force preference to use direct paths first, make indirect
-            // links as expensive as the linear vertex traversal.
-            return edge.link().type() == INDIRECT ? indirectLinkCost : 1;
-        }
-    }
-
-    // Link weight for preventing traversal over indirect links.
-    private static class NoIndirectLinksWeight implements LinkWeight {
-        @Override
-        public double weight(TopologyEdge edge) {
-            return edge.link().type() == INDIRECT ? -1 : 1;
-        }
-    }
-
-    @Override
-    public String toString() {
-        return toStringHelper(this)
-                .add("time", time)
-                .add("clusters", clusterCount())
-                .add("devices", deviceCount())
-                .add("links", linkCount())
-                .add("pathCount", pathCount())
-                .toString();
-    }
-}
diff --git a/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/DefaultTopologyGraph.java b/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/DefaultTopologyGraph.java
deleted file mode 100644
index 945ba05..0000000
--- a/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/DefaultTopologyGraph.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.onlab.onos.store.topology.impl;
-
-import org.onlab.graph.AdjacencyListsGraph;
-import org.onlab.onos.net.topology.TopologyEdge;
-import org.onlab.onos.net.topology.TopologyGraph;
-import org.onlab.onos.net.topology.TopologyVertex;
-
-import java.util.Set;
-
-/**
- * Default implementation of an immutable topology graph based on a generic
- * implementation of adjacency lists graph.
- */
-public class DefaultTopologyGraph
-        extends AdjacencyListsGraph<TopologyVertex, TopologyEdge>
-        implements TopologyGraph {
-
-    /**
-     * Creates a topology graph comprising of the specified vertexes and edges.
-     *
-     * @param vertexes set of graph vertexes
-     * @param edges    set of graph edges
-     */
-    public DefaultTopologyGraph(Set<TopologyVertex> vertexes, Set<TopologyEdge> edges) {
-        super(vertexes, edges);
-    }
-
-}
diff --git a/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/DistributedTopologyStore.java b/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/DistributedTopologyStore.java
deleted file mode 100644
index 04f5fce..0000000
--- a/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/DistributedTopologyStore.java
+++ /dev/null
@@ -1,142 +0,0 @@
-package org.onlab.onos.store.topology.impl;
-
-import static org.slf4j.LoggerFactory.getLogger;
-
-import java.util.List;
-import java.util.Set;
-
-import org.apache.felix.scr.annotations.Activate;
-import org.apache.felix.scr.annotations.Component;
-import org.apache.felix.scr.annotations.Deactivate;
-import org.apache.felix.scr.annotations.Service;
-import org.onlab.onos.event.Event;
-import org.onlab.onos.net.ConnectPoint;
-import org.onlab.onos.net.DeviceId;
-import org.onlab.onos.net.Link;
-import org.onlab.onos.net.Path;
-import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.net.topology.ClusterId;
-import org.onlab.onos.net.topology.GraphDescription;
-import org.onlab.onos.net.topology.LinkWeight;
-import org.onlab.onos.net.topology.Topology;
-import org.onlab.onos.net.topology.TopologyCluster;
-import org.onlab.onos.net.topology.TopologyEvent;
-import org.onlab.onos.net.topology.TopologyGraph;
-import org.onlab.onos.net.topology.TopologyStore;
-import org.onlab.onos.net.topology.TopologyStoreDelegate;
-import org.onlab.onos.store.AbstractStore;
-import org.slf4j.Logger;
-
-/**
- * TEMPORARY: Manages inventory of topology snapshots using distributed
- * structures implementation.
- */
-//FIXME: I LIE I AM NOT DISTRIBUTED
-@Component(immediate = true)
-@Service
-public class DistributedTopologyStore
-extends AbstractStore<TopologyEvent, TopologyStoreDelegate>
-implements TopologyStore {
-
-    private final Logger log = getLogger(getClass());
-
-    private volatile DefaultTopology current;
-
-    @Activate
-    public void activate() {
-        log.info("Started");
-    }
-
-    @Deactivate
-    public void deactivate() {
-        log.info("Stopped");
-    }
-    @Override
-    public Topology currentTopology() {
-        return current;
-    }
-
-    @Override
-    public boolean isLatest(Topology topology) {
-        // Topology is current only if it is the same as our current topology
-        return topology == current;
-    }
-
-    @Override
-    public TopologyGraph getGraph(Topology topology) {
-        return defaultTopology(topology).getGraph();
-    }
-
-    @Override
-    public Set<TopologyCluster> getClusters(Topology topology) {
-        return defaultTopology(topology).getClusters();
-    }
-
-    @Override
-    public TopologyCluster getCluster(Topology topology, ClusterId clusterId) {
-        return defaultTopology(topology).getCluster(clusterId);
-    }
-
-    @Override
-    public Set<DeviceId> getClusterDevices(Topology topology, TopologyCluster cluster) {
-        return defaultTopology(topology).getClusterDevices(cluster);
-    }
-
-    @Override
-    public Set<Link> getClusterLinks(Topology topology, TopologyCluster cluster) {
-        return defaultTopology(topology).getClusterLinks(cluster);
-    }
-
-    @Override
-    public Set<Path> getPaths(Topology topology, DeviceId src, DeviceId dst) {
-        return defaultTopology(topology).getPaths(src, dst);
-    }
-
-    @Override
-    public Set<Path> getPaths(Topology topology, DeviceId src, DeviceId dst,
-            LinkWeight weight) {
-        return defaultTopology(topology).getPaths(src, dst, weight);
-    }
-
-    @Override
-    public boolean isInfrastructure(Topology topology, ConnectPoint connectPoint) {
-        return defaultTopology(topology).isInfrastructure(connectPoint);
-    }
-
-    @Override
-    public boolean isBroadcastPoint(Topology topology, ConnectPoint connectPoint) {
-        return defaultTopology(topology).isBroadcastPoint(connectPoint);
-    }
-
-    @Override
-    public TopologyEvent updateTopology(ProviderId providerId,
-            GraphDescription graphDescription,
-            List<Event> reasons) {
-        // First off, make sure that what we're given is indeed newer than
-        // what we already have.
-        if (current != null && graphDescription.timestamp() < current.time()) {
-            return null;
-        }
-
-        // Have the default topology construct self from the description data.
-        DefaultTopology newTopology =
-                new DefaultTopology(providerId, graphDescription);
-
-        // Promote the new topology to current and return a ready-to-send event.
-        synchronized (this) {
-            current = newTopology;
-            return new TopologyEvent(TopologyEvent.Type.TOPOLOGY_CHANGED,
-                                     current, reasons);
-        }
-    }
-
-    // Validates the specified topology and returns it as a default
-    private DefaultTopology defaultTopology(Topology topology) {
-        if (topology instanceof DefaultTopology) {
-            return (DefaultTopology) topology;
-        }
-        throw new IllegalArgumentException("Topology class " + topology.getClass() +
-                " not supported");
-    }
-
-}
diff --git a/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/PathKey.java b/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/PathKey.java
deleted file mode 100644
index 60736b9..0000000
--- a/core/store/hz/net/src/main/java/org/onlab/onos/store/topology/impl/PathKey.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.onlab.onos.store.topology.impl;
-
-import org.onlab.onos.net.DeviceId;
-
-import java.util.Objects;
-
-/**
- * Key for filing pre-computed paths between source and destination devices.
- */
-class PathKey {
-    private final DeviceId src;
-    private final DeviceId dst;
-
-    /**
-     * Creates a path key from the given source/dest pair.
-     * @param src source device
-     * @param dst destination device
-     */
-    PathKey(DeviceId src, DeviceId dst) {
-        this.src = src;
-        this.dst = dst;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(src, dst);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj instanceof PathKey) {
-            final PathKey other = (PathKey) obj;
-            return Objects.equals(this.src, other.src) && Objects.equals(this.dst, other.dst);
-        }
-        return false;
-    }
-}
diff --git a/core/store/hz/net/src/test/java/org/onlab/onos/store/device/impl/DistributedDeviceStoreTest.java b/core/store/hz/net/src/test/java/org/onlab/onos/store/device/impl/DistributedDeviceStoreTest.java
deleted file mode 100644
index 7e2924b..0000000
--- a/core/store/hz/net/src/test/java/org/onlab/onos/store/device/impl/DistributedDeviceStoreTest.java
+++ /dev/null
@@ -1,390 +0,0 @@
-/**
- *
- */
-package org.onlab.onos.store.device.impl;
-
-import static org.junit.Assert.*;
-import static org.onlab.onos.net.Device.Type.SWITCH;
-import static org.onlab.onos.net.DeviceId.deviceId;
-import static org.onlab.onos.net.device.DeviceEvent.Type.*;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.onlab.onos.net.Device;
-import org.onlab.onos.net.DeviceId;
-import org.onlab.onos.net.Port;
-import org.onlab.onos.net.PortNumber;
-import org.onlab.onos.net.device.DefaultDeviceDescription;
-import org.onlab.onos.net.device.DefaultPortDescription;
-import org.onlab.onos.net.device.DeviceDescription;
-import org.onlab.onos.net.device.DeviceEvent;
-import org.onlab.onos.net.device.DeviceStoreDelegate;
-import org.onlab.onos.net.device.PortDescription;
-import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.common.StoreManager;
-import org.onlab.onos.store.common.StoreService;
-import org.onlab.onos.store.common.TestStoreManager;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
-import com.hazelcast.config.Config;
-import com.hazelcast.core.Hazelcast;
-
-/**
- * Test of the Hazelcast based distributed DeviceStore implementation.
- */
-public class DistributedDeviceStoreTest {
-
-    private static final ProviderId PID = new ProviderId("of", "foo");
-    private static final DeviceId DID1 = deviceId("of:foo");
-    private static final DeviceId DID2 = deviceId("of:bar");
-    private static final String MFR = "whitebox";
-    private static final String HW = "1.1.x";
-    private static final String SW1 = "3.8.1";
-    private static final String SW2 = "3.9.5";
-    private static final String SN = "43311-12345";
-
-    private static final PortNumber P1 = PortNumber.portNumber(1);
-    private static final PortNumber P2 = PortNumber.portNumber(2);
-    private static final PortNumber P3 = PortNumber.portNumber(3);
-
-    private DistributedDeviceStore deviceStore;
-
-    private StoreManager storeManager;
-
-
-    @BeforeClass
-    public static void setUpBeforeClass() throws Exception {
-    }
-
-    @AfterClass
-    public static void tearDownAfterClass() throws Exception {
-    }
-
-
-    @Before
-    public void setUp() throws Exception {
-        // TODO should find a way to clean Hazelcast instance without shutdown.
-        Config config = TestStoreManager.getTestConfig();
-
-        storeManager = new TestStoreManager(Hazelcast.newHazelcastInstance(config));
-        storeManager.activate();
-
-        deviceStore = new TestDistributedDeviceStore(storeManager);
-        deviceStore.activate();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        deviceStore.deactivate();
-
-        storeManager.deactivate();
-    }
-
-    private void putDevice(DeviceId deviceId, String swVersion) {
-        DeviceDescription description =
-                new DefaultDeviceDescription(deviceId.uri(), SWITCH, MFR,
-                        HW, swVersion, SN);
-        deviceStore.createOrUpdateDevice(PID, deviceId, description);
-    }
-
-    private static void assertDevice(DeviceId id, String swVersion, Device device) {
-        assertNotNull(device);
-        assertEquals(id, device.id());
-        assertEquals(MFR, device.manufacturer());
-        assertEquals(HW, device.hwVersion());
-        assertEquals(swVersion, device.swVersion());
-        assertEquals(SN, device.serialNumber());
-    }
-
-    @Test
-    public final void testGetDeviceCount() {
-        assertEquals("initialy empty", 0, deviceStore.getDeviceCount());
-
-        putDevice(DID1, SW1);
-        putDevice(DID2, SW2);
-        putDevice(DID1, SW1);
-
-        assertEquals("expect 2 uniq devices", 2, deviceStore.getDeviceCount());
-    }
-
-    @Test
-    public final void testGetDevices() {
-        assertEquals("initialy empty", 0, Iterables.size(deviceStore.getDevices()));
-
-        putDevice(DID1, SW1);
-        putDevice(DID2, SW2);
-        putDevice(DID1, SW1);
-
-        assertEquals("expect 2 uniq devices",
-                2, Iterables.size(deviceStore.getDevices()));
-
-        Map<DeviceId, Device> devices = new HashMap<>();
-        for (Device device : deviceStore.getDevices()) {
-            devices.put(device.id(), device);
-        }
-
-        assertDevice(DID1, SW1, devices.get(DID1));
-        assertDevice(DID2, SW2, devices.get(DID2));
-
-        // add case for new node?
-    }
-
-    @Test
-    public final void testGetDevice() {
-
-        putDevice(DID1, SW1);
-
-        assertDevice(DID1, SW1, deviceStore.getDevice(DID1));
-        assertNull("DID2 shouldn't be there", deviceStore.getDevice(DID2));
-    }
-
-    @Test
-    public final void testCreateOrUpdateDevice() {
-        DeviceDescription description =
-                new DefaultDeviceDescription(DID1.uri(), SWITCH, MFR,
-                        HW, SW1, SN);
-        DeviceEvent event = deviceStore.createOrUpdateDevice(PID, DID1, description);
-        assertEquals(DEVICE_ADDED, event.type());
-        assertDevice(DID1, SW1, event.subject());
-
-        DeviceDescription description2 =
-                new DefaultDeviceDescription(DID1.uri(), SWITCH, MFR,
-                        HW, SW2, SN);
-        DeviceEvent event2 = deviceStore.createOrUpdateDevice(PID, DID1, description2);
-        assertEquals(DEVICE_UPDATED, event2.type());
-        assertDevice(DID1, SW2, event2.subject());
-
-        assertNull("No change expected", deviceStore.createOrUpdateDevice(PID, DID1, description2));
-    }
-
-    @Test
-    public final void testMarkOffline() {
-
-        putDevice(DID1, SW1);
-        assertTrue(deviceStore.isAvailable(DID1));
-
-        DeviceEvent event = deviceStore.markOffline(DID1);
-        assertEquals(DEVICE_AVAILABILITY_CHANGED, event.type());
-        assertDevice(DID1, SW1, event.subject());
-        assertFalse(deviceStore.isAvailable(DID1));
-
-        DeviceEvent event2 = deviceStore.markOffline(DID1);
-        assertNull("No change, no event", event2);
-}
-
-    @Test
-    public final void testUpdatePorts() {
-        putDevice(DID1, SW1);
-        List<PortDescription> pds = Arrays.<PortDescription>asList(
-                new DefaultPortDescription(P1, true),
-                new DefaultPortDescription(P2, true)
-                );
-
-        List<DeviceEvent> events = deviceStore.updatePorts(PID, DID1, pds);
-
-        Set<PortNumber> expectedPorts = Sets.newHashSet(P1, P2);
-        for (DeviceEvent event : events) {
-            assertEquals(PORT_ADDED, event.type());
-            assertDevice(DID1, SW1, event.subject());
-            assertTrue("PortNumber is one of expected",
-                    expectedPorts.remove(event.port().number()));
-            assertTrue("Port is enabled", event.port().isEnabled());
-        }
-        assertTrue("Event for all expectedport appeared", expectedPorts.isEmpty());
-
-
-        List<PortDescription> pds2 = Arrays.<PortDescription>asList(
-                new DefaultPortDescription(P1, false),
-                new DefaultPortDescription(P2, true),
-                new DefaultPortDescription(P3, true)
-                );
-
-        events = deviceStore.updatePorts(PID, DID1, pds2);
-        assertFalse("event should be triggered", events.isEmpty());
-        for (DeviceEvent event : events) {
-            PortNumber num = event.port().number();
-            if (P1.equals(num)) {
-                assertEquals(PORT_UPDATED, event.type());
-                assertDevice(DID1, SW1, event.subject());
-                assertFalse("Port is disabled", event.port().isEnabled());
-            } else if (P2.equals(num)) {
-                fail("P2 event not expected.");
-            } else if (P3.equals(num)) {
-                assertEquals(PORT_ADDED, event.type());
-                assertDevice(DID1, SW1, event.subject());
-                assertTrue("Port is enabled", event.port().isEnabled());
-            } else {
-                fail("Unknown port number encountered: " + num);
-            }
-        }
-
-        List<PortDescription> pds3 = Arrays.<PortDescription>asList(
-                new DefaultPortDescription(P1, false),
-                new DefaultPortDescription(P2, true)
-                );
-        events = deviceStore.updatePorts(PID, DID1, pds3);
-        assertFalse("event should be triggered", events.isEmpty());
-        for (DeviceEvent event : events) {
-            PortNumber num = event.port().number();
-            if (P1.equals(num)) {
-                fail("P1 event not expected.");
-            } else if (P2.equals(num)) {
-                fail("P2 event not expected.");
-            } else if (P3.equals(num)) {
-                assertEquals(PORT_REMOVED, event.type());
-                assertDevice(DID1, SW1, event.subject());
-                assertTrue("Port was enabled", event.port().isEnabled());
-            } else {
-                fail("Unknown port number encountered: " + num);
-            }
-        }
-
-    }
-
-    @Test
-    public final void testUpdatePortStatus() {
-        putDevice(DID1, SW1);
-        List<PortDescription> pds = Arrays.<PortDescription>asList(
-                new DefaultPortDescription(P1, true)
-                );
-        deviceStore.updatePorts(PID, DID1, pds);
-
-        DeviceEvent event = deviceStore.updatePortStatus(PID, DID1,
-                new DefaultPortDescription(P1, false));
-        assertEquals(PORT_UPDATED, event.type());
-        assertDevice(DID1, SW1, event.subject());
-        assertEquals(P1, event.port().number());
-        assertFalse("Port is disabled", event.port().isEnabled());
-    }
-
-    @Test
-    public final void testGetPorts() {
-        putDevice(DID1, SW1);
-        putDevice(DID2, SW1);
-        List<PortDescription> pds = Arrays.<PortDescription>asList(
-                new DefaultPortDescription(P1, true),
-                new DefaultPortDescription(P2, true)
-                );
-        deviceStore.updatePorts(PID, DID1, pds);
-
-        Set<PortNumber> expectedPorts = Sets.newHashSet(P1, P2);
-        List<Port> ports = deviceStore.getPorts(DID1);
-        for (Port port : ports) {
-            assertTrue("Port is enabled", port.isEnabled());
-            assertTrue("PortNumber is one of expected",
-                    expectedPorts.remove(port.number()));
-        }
-        assertTrue("Event for all expectedport appeared", expectedPorts.isEmpty());
-
-
-        assertTrue("DID2 has no ports", deviceStore.getPorts(DID2).isEmpty());
-    }
-
-    @Test
-    public final void testGetPort() {
-        putDevice(DID1, SW1);
-        putDevice(DID2, SW1);
-        List<PortDescription> pds = Arrays.<PortDescription>asList(
-                new DefaultPortDescription(P1, true),
-                new DefaultPortDescription(P2, false)
-                );
-        deviceStore.updatePorts(PID, DID1, pds);
-
-        Port port1 = deviceStore.getPort(DID1, P1);
-        assertEquals(P1, port1.number());
-        assertTrue("Port is enabled", port1.isEnabled());
-
-        Port port2 = deviceStore.getPort(DID1, P2);
-        assertEquals(P2, port2.number());
-        assertFalse("Port is disabled", port2.isEnabled());
-
-        Port port3 = deviceStore.getPort(DID1, P3);
-        assertNull("P3 not expected", port3);
-    }
-
-    @Test
-    public final void testRemoveDevice() {
-        putDevice(DID1, SW1);
-        putDevice(DID2, SW1);
-
-        assertEquals(2, deviceStore.getDeviceCount());
-
-        DeviceEvent event = deviceStore.removeDevice(DID1);
-        assertEquals(DEVICE_REMOVED, event.type());
-        assertDevice(DID1, SW1, event.subject());
-
-        assertEquals(1, deviceStore.getDeviceCount());
-    }
-
-    // TODO add test for Port events when we have them
-    @Ignore("Ignore until Delegate spec. is clear.")
-    @Test
-    public final void testEvents() throws InterruptedException {
-        final CountDownLatch addLatch = new CountDownLatch(1);
-        DeviceStoreDelegate checkAdd = new DeviceStoreDelegate() {
-            @Override
-            public void notify(DeviceEvent event) {
-                assertEquals(DEVICE_ADDED, event.type());
-                assertDevice(DID1, SW1, event.subject());
-                addLatch.countDown();
-            }
-        };
-        final CountDownLatch updateLatch = new CountDownLatch(1);
-        DeviceStoreDelegate checkUpdate = new DeviceStoreDelegate() {
-            @Override
-            public void notify(DeviceEvent event) {
-                assertEquals(DEVICE_UPDATED, event.type());
-                assertDevice(DID1, SW2, event.subject());
-                updateLatch.countDown();
-            }
-        };
-        final CountDownLatch removeLatch = new CountDownLatch(1);
-        DeviceStoreDelegate checkRemove = new DeviceStoreDelegate() {
-            @Override
-            public void notify(DeviceEvent event) {
-                assertEquals(DEVICE_REMOVED, event.type());
-                assertDevice(DID1, SW2, event.subject());
-                removeLatch.countDown();
-            }
-        };
-
-        DeviceDescription description =
-                new DefaultDeviceDescription(DID1.uri(), SWITCH, MFR,
-                        HW, SW1, SN);
-        deviceStore.setDelegate(checkAdd);
-        deviceStore.createOrUpdateDevice(PID, DID1, description);
-        assertTrue("Add event fired", addLatch.await(1, TimeUnit.SECONDS));
-
-
-        DeviceDescription description2 =
-                new DefaultDeviceDescription(DID1.uri(), SWITCH, MFR,
-                        HW, SW2, SN);
-        deviceStore.unsetDelegate(checkAdd);
-        deviceStore.setDelegate(checkUpdate);
-        deviceStore.createOrUpdateDevice(PID, DID1, description2);
-        assertTrue("Update event fired", updateLatch.await(1, TimeUnit.SECONDS));
-
-        deviceStore.unsetDelegate(checkUpdate);
-        deviceStore.setDelegate(checkRemove);
-        deviceStore.removeDevice(DID1);
-        assertTrue("Remove event fired", removeLatch.await(1, TimeUnit.SECONDS));
-    }
-
-    private class TestDistributedDeviceStore extends DistributedDeviceStore {
-        public TestDistributedDeviceStore(StoreService storeService) {
-            this.storeService = storeService;
-        }
-    }
-}
diff --git a/core/store/hz/net/src/test/java/org/onlab/onos/store/link/impl/DistributedLinkStoreTest.java b/core/store/hz/net/src/test/java/org/onlab/onos/store/link/impl/DistributedLinkStoreTest.java
deleted file mode 100644
index 7415fed..0000000
--- a/core/store/hz/net/src/test/java/org/onlab/onos/store/link/impl/DistributedLinkStoreTest.java
+++ /dev/null
@@ -1,361 +0,0 @@
-package org.onlab.onos.store.link.impl;
-
-import static org.junit.Assert.*;
-import static org.onlab.onos.net.DeviceId.deviceId;
-import static org.onlab.onos.net.Link.Type.*;
-import static org.onlab.onos.net.LinkKey.linkKey;
-import static org.onlab.onos.net.link.LinkEvent.Type.*;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.onlab.onos.net.ConnectPoint;
-import org.onlab.onos.net.DeviceId;
-import org.onlab.onos.net.Link;
-import org.onlab.onos.net.LinkKey;
-import org.onlab.onos.net.PortNumber;
-import org.onlab.onos.net.Link.Type;
-import org.onlab.onos.net.link.DefaultLinkDescription;
-import org.onlab.onos.net.link.LinkEvent;
-import org.onlab.onos.net.link.LinkStoreDelegate;
-import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.common.StoreManager;
-import org.onlab.onos.store.common.StoreService;
-import org.onlab.onos.store.common.TestStoreManager;
-import com.google.common.collect.Iterables;
-import com.hazelcast.config.Config;
-import com.hazelcast.core.Hazelcast;
-
-/**
- * Test of the Hazelcast based distributed LinkStore implementation.
- */
-public class DistributedLinkStoreTest {
-
-    private static final ProviderId PID = new ProviderId("of", "foo");
-    private static final DeviceId DID1 = deviceId("of:foo");
-    private static final DeviceId DID2 = deviceId("of:bar");
-
-    private static final PortNumber P1 = PortNumber.portNumber(1);
-    private static final PortNumber P2 = PortNumber.portNumber(2);
-    private static final PortNumber P3 = PortNumber.portNumber(3);
-
-    private StoreManager storeManager;
-
-    private DistributedLinkStore linkStore;
-
-    @BeforeClass
-    public static void setUpBeforeClass() throws Exception {
-    }
-
-    @AfterClass
-    public static void tearDownAfterClass() throws Exception {
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        // TODO should find a way to clean Hazelcast instance without shutdown.
-        Config config = TestStoreManager.getTestConfig();
-
-        storeManager = new TestStoreManager(Hazelcast.newHazelcastInstance(config));
-        storeManager.activate();
-
-        linkStore = new TestDistributedLinkStore(storeManager);
-        linkStore.activate();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        linkStore.deactivate();
-        storeManager.deactivate();
-    }
-
-    private void putLink(DeviceId srcId, PortNumber srcNum,
-                         DeviceId dstId, PortNumber dstNum, Type type) {
-        ConnectPoint src = new ConnectPoint(srcId, srcNum);
-        ConnectPoint dst = new ConnectPoint(dstId, dstNum);
-        linkStore.createOrUpdateLink(PID, new DefaultLinkDescription(src, dst, type));
-    }
-
-    private void putLink(LinkKey key, Type type) {
-        putLink(key.src().deviceId(), key.src().port(),
-                key.dst().deviceId(), key.dst().port(),
-                type);
-    }
-
-    private static void assertLink(DeviceId srcId, PortNumber srcNum,
-                            DeviceId dstId, PortNumber dstNum, Type type,
-                            Link link) {
-        assertEquals(srcId, link.src().deviceId());
-        assertEquals(srcNum, link.src().port());
-        assertEquals(dstId, link.dst().deviceId());
-        assertEquals(dstNum, link.dst().port());
-        assertEquals(type, link.type());
-    }
-
-    private static void assertLink(LinkKey key, Type type, Link link) {
-        assertLink(key.src().deviceId(), key.src().port(),
-                   key.dst().deviceId(), key.dst().port(),
-                   type, link);
-    }
-
-    @Test
-    public final void testGetLinkCount() {
-        assertEquals("initialy empty", 0, linkStore.getLinkCount());
-
-        putLink(DID1, P1, DID2, P2, DIRECT);
-        putLink(DID2, P2, DID1, P1, DIRECT);
-        putLink(DID1, P1, DID2, P2, DIRECT);
-
-        assertEquals("expecting 2 unique link", 2, linkStore.getLinkCount());
-    }
-
-    @Test
-    public final void testGetLinks() {
-        assertEquals("initialy empty", 0,
-                Iterables.size(linkStore.getLinks()));
-
-        LinkKey linkId1 = linkKey(new ConnectPoint(DID1, P1), new ConnectPoint(DID2, P2));
-        LinkKey linkId2 = linkKey(new ConnectPoint(DID2, P2), new ConnectPoint(DID1, P1));
-
-        putLink(linkId1, DIRECT);
-        putLink(linkId2, DIRECT);
-        putLink(linkId1, DIRECT);
-
-        assertEquals("expecting 2 unique link", 2,
-                Iterables.size(linkStore.getLinks()));
-
-        Map<LinkKey, Link> links = new HashMap<>();
-        for (Link link : linkStore.getLinks()) {
-            links.put(linkKey(link), link);
-        }
-
-        assertLink(linkId1, DIRECT, links.get(linkId1));
-        assertLink(linkId2, DIRECT, links.get(linkId2));
-    }
-
-    @Test
-    public final void testGetDeviceEgressLinks() {
-        LinkKey linkId1 = linkKey(new ConnectPoint(DID1, P1), new ConnectPoint(DID2, P2));
-        LinkKey linkId2 = linkKey(new ConnectPoint(DID2, P2), new ConnectPoint(DID1, P1));
-        LinkKey linkId3 = linkKey(new ConnectPoint(DID1, P2), new ConnectPoint(DID2, P3));
-
-        putLink(linkId1, DIRECT);
-        putLink(linkId2, DIRECT);
-        putLink(linkId3, DIRECT);
-
-        // DID1,P1 => DID2,P2
-        // DID2,P2 => DID1,P1
-        // DID1,P2 => DID2,P3
-
-        Set<Link> links1 = linkStore.getDeviceEgressLinks(DID1);
-        assertEquals(2, links1.size());
-        // check
-
-        Set<Link> links2 = linkStore.getDeviceEgressLinks(DID2);
-        assertEquals(1, links2.size());
-        assertLink(linkId2, DIRECT, links2.iterator().next());
-    }
-
-    @Test
-    public final void testGetDeviceIngressLinks() {
-        LinkKey linkId1 = linkKey(new ConnectPoint(DID1, P1), new ConnectPoint(DID2, P2));
-        LinkKey linkId2 = linkKey(new ConnectPoint(DID2, P2), new ConnectPoint(DID1, P1));
-        LinkKey linkId3 = linkKey(new ConnectPoint(DID1, P2), new ConnectPoint(DID2, P3));
-
-        putLink(linkId1, DIRECT);
-        putLink(linkId2, DIRECT);
-        putLink(linkId3, DIRECT);
-
-        // DID1,P1 => DID2,P2
-        // DID2,P2 => DID1,P1
-        // DID1,P2 => DID2,P3
-
-        Set<Link> links1 = linkStore.getDeviceIngressLinks(DID2);
-        assertEquals(2, links1.size());
-        // check
-
-        Set<Link> links2 = linkStore.getDeviceIngressLinks(DID1);
-        assertEquals(1, links2.size());
-        assertLink(linkId2, DIRECT, links2.iterator().next());
-    }
-
-    @Test
-    public final void testGetLink() {
-        ConnectPoint src = new ConnectPoint(DID1, P1);
-        ConnectPoint dst = new ConnectPoint(DID2, P2);
-        LinkKey linkId1 = linkKey(src, dst);
-
-        putLink(linkId1, DIRECT);
-
-        Link link = linkStore.getLink(src, dst);
-        assertLink(linkId1, DIRECT, link);
-
-        assertNull("There shouldn't be reverese link",
-                linkStore.getLink(dst, src));
-    }
-
-    @Test
-    public final void testGetEgressLinks() {
-        final ConnectPoint d1P1 = new ConnectPoint(DID1, P1);
-        final ConnectPoint d2P2 = new ConnectPoint(DID2, P2);
-        LinkKey linkId1 = linkKey(d1P1, d2P2);
-        LinkKey linkId2 = linkKey(d2P2, d1P1);
-        LinkKey linkId3 = linkKey(new ConnectPoint(DID1, P2), new ConnectPoint(DID2, P3));
-
-        putLink(linkId1, DIRECT);
-        putLink(linkId2, DIRECT);
-        putLink(linkId3, DIRECT);
-
-        // DID1,P1 => DID2,P2
-        // DID2,P2 => DID1,P1
-        // DID1,P2 => DID2,P3
-
-        Set<Link> links1 = linkStore.getEgressLinks(d1P1);
-        assertEquals(1, links1.size());
-        assertLink(linkId1, DIRECT, links1.iterator().next());
-
-        Set<Link> links2 = linkStore.getEgressLinks(d2P2);
-        assertEquals(1, links2.size());
-        assertLink(linkId2, DIRECT, links2.iterator().next());
-    }
-
-    @Test
-    public final void testGetIngressLinks() {
-        final ConnectPoint d1P1 = new ConnectPoint(DID1, P1);
-        final ConnectPoint d2P2 = new ConnectPoint(DID2, P2);
-        LinkKey linkId1 = linkKey(d1P1, d2P2);
-        LinkKey linkId2 = linkKey(d2P2, d1P1);
-        LinkKey linkId3 = linkKey(new ConnectPoint(DID1, P2), new ConnectPoint(DID2, P3));
-
-        putLink(linkId1, DIRECT);
-        putLink(linkId2, DIRECT);
-        putLink(linkId3, DIRECT);
-
-        // DID1,P1 => DID2,P2
-        // DID2,P2 => DID1,P1
-        // DID1,P2 => DID2,P3
-
-        Set<Link> links1 = linkStore.getIngressLinks(d2P2);
-        assertEquals(1, links1.size());
-        assertLink(linkId1, DIRECT, links1.iterator().next());
-
-        Set<Link> links2 = linkStore.getIngressLinks(d1P1);
-        assertEquals(1, links2.size());
-        assertLink(linkId2, DIRECT, links2.iterator().next());
-    }
-
-    @Test
-    public final void testCreateOrUpdateLink() {
-        ConnectPoint src = new ConnectPoint(DID1, P1);
-        ConnectPoint dst = new ConnectPoint(DID2, P2);
-
-        // add link
-        LinkEvent event = linkStore.createOrUpdateLink(PID,
-                    new DefaultLinkDescription(src, dst, INDIRECT));
-
-        assertLink(DID1, P1, DID2, P2, INDIRECT, event.subject());
-        assertEquals(LINK_ADDED, event.type());
-
-        // update link type
-        LinkEvent event2 = linkStore.createOrUpdateLink(PID,
-                new DefaultLinkDescription(src, dst, DIRECT));
-
-        assertLink(DID1, P1, DID2, P2, DIRECT, event2.subject());
-        assertEquals(LINK_UPDATED, event2.type());
-
-        // no change
-        LinkEvent event3 = linkStore.createOrUpdateLink(PID,
-                new DefaultLinkDescription(src, dst, DIRECT));
-
-        assertNull("No change event expected", event3);
-    }
-
-    @Test
-    public final void testRemoveLink() {
-        final ConnectPoint d1P1 = new ConnectPoint(DID1, P1);
-        final ConnectPoint d2P2 = new ConnectPoint(DID2, P2);
-        LinkKey linkId1 = linkKey(d1P1, d2P2);
-        LinkKey linkId2 = linkKey(d2P2, d1P1);
-
-        putLink(linkId1, DIRECT);
-        putLink(linkId2, DIRECT);
-
-        // DID1,P1 => DID2,P2
-        // DID2,P2 => DID1,P1
-        // DID1,P2 => DID2,P3
-
-        LinkEvent event = linkStore.removeLink(d1P1, d2P2);
-        assertEquals(LINK_REMOVED, event.type());
-        LinkEvent event2 = linkStore.removeLink(d1P1, d2P2);
-        assertNull(event2);
-
-        assertLink(linkId2, DIRECT, linkStore.getLink(d2P2, d1P1));
-    }
-
-    @Ignore("Ignore until Delegate spec. is clear.")
-    @Test
-    public final void testEvents() throws InterruptedException {
-
-        final ConnectPoint d1P1 = new ConnectPoint(DID1, P1);
-        final ConnectPoint d2P2 = new ConnectPoint(DID2, P2);
-        final LinkKey linkId1 = linkKey(d1P1, d2P2);
-
-        final CountDownLatch addLatch = new CountDownLatch(1);
-        LinkStoreDelegate checkAdd = new LinkStoreDelegate() {
-            @Override
-            public void notify(LinkEvent event) {
-                assertEquals(LINK_ADDED, event.type());
-                assertLink(linkId1, INDIRECT, event.subject());
-                addLatch.countDown();
-            }
-        };
-        final CountDownLatch updateLatch = new CountDownLatch(1);
-        LinkStoreDelegate checkUpdate = new LinkStoreDelegate() {
-            @Override
-            public void notify(LinkEvent event) {
-                assertEquals(LINK_UPDATED, event.type());
-                assertLink(linkId1, DIRECT, event.subject());
-                updateLatch.countDown();
-            }
-        };
-        final CountDownLatch removeLatch = new CountDownLatch(1);
-        LinkStoreDelegate checkRemove = new LinkStoreDelegate() {
-            @Override
-            public void notify(LinkEvent event) {
-                assertEquals(LINK_REMOVED, event.type());
-                assertLink(linkId1, DIRECT, event.subject());
-                removeLatch.countDown();
-            }
-        };
-
-        linkStore.setDelegate(checkAdd);
-        putLink(linkId1, INDIRECT);
-        assertTrue("Add event fired", addLatch.await(1, TimeUnit.SECONDS));
-
-        linkStore.unsetDelegate(checkAdd);
-        linkStore.setDelegate(checkUpdate);
-        putLink(linkId1, DIRECT);
-        assertTrue("Update event fired", updateLatch.await(1, TimeUnit.SECONDS));
-
-        linkStore.unsetDelegate(checkUpdate);
-        linkStore.setDelegate(checkRemove);
-        linkStore.removeLink(d1P1, d2P2);
-        assertTrue("Remove event fired", removeLatch.await(1, TimeUnit.SECONDS));
-    }
-
-
-    class TestDistributedLinkStore extends DistributedLinkStore {
-        TestDistributedLinkStore(StoreService storeService) {
-            this.storeService = storeService;
-        }
-    }
-}
diff --git a/core/store/hz/pom.xml b/core/store/hz/pom.xml
index d6aa1fe..70ff03f 100644
--- a/core/store/hz/pom.xml
+++ b/core/store/hz/pom.xml
@@ -17,7 +17,6 @@
     <modules>
         <module>common</module>
         <module>cluster</module>
-        <module>net</module>
     </modules>
 
     <dependencies>
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/HostLocationSerializer.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/HostLocationSerializer.java
new file mode 100644
index 0000000..2ef09ac
--- /dev/null
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/HostLocationSerializer.java
@@ -0,0 +1,40 @@
+package org.onlab.onos.store.serializers;
+
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.HostLocation;
+import org.onlab.onos.net.PortNumber;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+/**
+* Kryo Serializer for {@link HostLocation}.
+*/
+public class HostLocationSerializer extends Serializer<HostLocation> {
+
+    /**
+     * Creates {@link HostLocation} serializer instance.
+     */
+    public HostLocationSerializer() {
+        // non-null, immutable
+        super(false, true);
+    }
+
+    @Override
+    public void write(Kryo kryo, Output output, HostLocation object) {
+        kryo.writeClassAndObject(output, object.deviceId());
+        kryo.writeClassAndObject(output, object.port());
+        output.writeLong(object.time());
+    }
+
+    @Override
+    public HostLocation read(Kryo kryo, Input input, Class<HostLocation> type) {
+        DeviceId deviceId = (DeviceId) kryo.readClassAndObject(input);
+        PortNumber portNumber = (PortNumber) kryo.readClassAndObject(input);
+        long time = input.readLong();
+        return new HostLocation(deviceId, portNumber, time);
+    }
+
+}
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/IpAddressSerializer.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/IpAddressSerializer.java
index b923df7..280da1e 100644
--- a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/IpAddressSerializer.java
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/IpAddressSerializer.java
@@ -20,8 +20,7 @@
     }
 
     @Override
-    public void write(Kryo kryo, Output output,
-            IpAddress object) {
+    public void write(Kryo kryo, Output output, IpAddress object) {
         byte[] octs = object.toOctets();
         output.writeInt(octs.length);
         output.writeBytes(octs);
@@ -29,11 +28,10 @@
     }
 
     @Override
-    public IpAddress read(Kryo kryo, Input input,
-            Class<IpAddress> type) {
-        int octLen = input.readInt();
+    public IpAddress read(Kryo kryo, Input input, Class<IpAddress> type) {
+        final int octLen = input.readInt();
         byte[] octs = new byte[octLen];
-        input.read(octs);
+        input.readBytes(octs);
         int prefLen = input.readInt();
         return IpAddress.valueOf(octs, prefLen);
     }
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/IpPrefixSerializer.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/IpPrefixSerializer.java
index 2e92692..cf82ebb 100644
--- a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/IpPrefixSerializer.java
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/IpPrefixSerializer.java
@@ -34,7 +34,7 @@
             Class<IpPrefix> type) {
         int octLen = input.readInt();
         byte[] octs = new byte[octLen];
-        input.read(octs);
+        input.readBytes(octs);
         int prefLen = input.readInt();
         return IpPrefix.valueOf(octs, prefLen);
     }
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoPoolUtil.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoPoolUtil.java
index 98b1ee3..38c4dfd 100644
--- a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoPoolUtil.java
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoPoolUtil.java
@@ -17,21 +17,28 @@
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.Element;
+import org.onlab.onos.net.HostId;
+import org.onlab.onos.net.HostLocation;
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.LinkKey;
 import org.onlab.onos.net.Port;
 import org.onlab.onos.net.PortNumber;
 import org.onlab.onos.net.device.DefaultDeviceDescription;
 import org.onlab.onos.net.device.DefaultPortDescription;
+import org.onlab.onos.net.host.DefaultHostDescription;
+import org.onlab.onos.net.host.HostDescription;
 import org.onlab.onos.net.link.DefaultLinkDescription;
 import org.onlab.onos.net.provider.ProviderId;
 import org.onlab.onos.store.Timestamp;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
 import org.onlab.util.KryoPool;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 
 public final class KryoPoolUtil {
 
@@ -41,6 +48,8 @@
     public static final KryoPool MISC = KryoPool.newBuilder()
             .register(IpPrefix.class, new IpPrefixSerializer())
             .register(IpAddress.class, new IpAddressSerializer())
+            .register(MacAddress.class, new MacAddressSerializer())
+            .register(VlanId.class)
             .build();
 
     // TODO: Populate other classes
@@ -51,6 +60,7 @@
             .register(MISC)
             .register(ImmutableMap.class, new ImmutableMapSerializer())
             .register(ImmutableList.class, new ImmutableListSerializer())
+            .register(ImmutableSet.class, new ImmutableSetSerializer())
             .register(
                     //
                     ArrayList.class,
@@ -69,8 +79,10 @@
                     DefaultPortDescription.class,
                     Element.class,
                     Link.Type.class,
-                    Timestamp.class
-
+                    Timestamp.class,
+                    HostId.class,
+                    HostDescription.class,
+                    DefaultHostDescription.class
                     )
             .register(URI.class, new URISerializer())
             .register(NodeId.class, new NodeIdSerializer())
@@ -82,6 +94,8 @@
             .register(ConnectPoint.class, new ConnectPointSerializer())
             .register(DefaultLink.class, new DefaultLinkSerializer())
             .register(MastershipTerm.class, new MastershipTermSerializer())
+            .register(MastershipRole.class, new MastershipRoleSerializer())
+            .register(HostLocation.class, new HostLocationSerializer())
 
             .build();
 
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/MacAddressSerializer.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/MacAddressSerializer.java
new file mode 100644
index 0000000..954c071
--- /dev/null
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/MacAddressSerializer.java
@@ -0,0 +1,32 @@
+package org.onlab.onos.store.serializers;
+
+import org.onlab.packet.MacAddress;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+/**
+ * Kryo Serializer for {@link MacAddress}.
+ */
+public class MacAddressSerializer extends Serializer<MacAddress> {
+
+    /**
+     * Creates {@link MacAddress} serializer instance.
+     */
+    public MacAddressSerializer() {
+        super(false, true);
+    }
+
+    @Override
+    public void write(Kryo kryo, Output output, MacAddress object) {
+        output.writeBytes(object.getAddress());
+    }
+
+    @Override
+    public MacAddress read(Kryo kryo, Input input, Class<MacAddress> type) {
+        return MacAddress.valueOf(input.readBytes(MacAddress.MAC_ADDRESS_LENGTH));
+    }
+
+}
diff --git a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleHostStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleHostStore.java
index bf99227..ef80b72 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleHostStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleHostStore.java
@@ -84,7 +84,7 @@
                                             descr.hwAddress(),
                                             descr.vlan(),
                                             descr.location(),
-                                            ImmutableSet.of(descr.ipAddress()));
+                                            ImmutableSet.copyOf(descr.ipAddress()));
         synchronized (this) {
             hosts.put(hostId, newhost);
             locations.put(descr.location(), newhost);
@@ -101,12 +101,12 @@
             return new HostEvent(HOST_MOVED, host);
         }
 
-        if (host.ipAddresses().contains(descr.ipAddress())) {
+        if (host.ipAddresses().containsAll(descr.ipAddress())) {
             return null;
         }
 
         Set<IpPrefix> addresses = new HashSet<>(host.ipAddresses());
-        addresses.add(descr.ipAddress());
+        addresses.addAll(descr.ipAddress());
         StoredHost updated = new StoredHost(providerId, host.id(),
                                             host.mac(), host.vlan(),
                                             descr.location(), addresses);
diff --git a/features/features.xml b/features/features.xml
index 34c70bc..f0430e4 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -30,6 +30,7 @@
 
 	<bundle>mvn:org.codehaus.jackson/jackson-core-asl/1.9.13</bundle>
 	<bundle>mvn:org.codehaus.jackson/jackson-mapper-asl/1.9.13</bundle>
+	<bundle>mvn:org.onlab.onos/onlab-thirdparty/1.0.0-SNAPSHOT</bundle>
     </feature>
 
     <feature name="onos-thirdparty-web" version="1.0.0"
diff --git a/pom.xml b/pom.xml
index 9b275ab..daeb33e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -107,7 +107,13 @@
             </dependency>
 
             <dependency>
-                <groupId>commons-lang</groupId>
+              <groupId>com.googlecode.concurrent-trees</groupId>
+              <artifactId>concurrent-trees</artifactId>
+              <version>2.4.0</version>
+            </dependency>
+
+            <dependency>
+              <groupId>commons-lang</groupId>
                 <artifactId>commons-lang</artifactId>
                 <version>2.6</version>
             </dependency>
@@ -164,6 +170,12 @@
                 <scope>provided</scope>
             </dependency>
             <dependency>
+                <groupId>org.osgi</groupId>
+                <artifactId>org.osgi.compendium</artifactId>
+                <version>4.3.1</version>
+                <scope>provided</scope>
+            </dependency>
+            <dependency>
                 <groupId>org.apache.felix</groupId>
                 <artifactId>org.apache.felix.scr.annotations</artifactId>
                 <version>1.9.8</version>
@@ -260,6 +272,13 @@
                 <artifactId>onos-of-api</artifactId>
                 <version>${project.version}</version>
             </dependency>
+
+            <dependency>
+              <groupId>org.onlab.onos</groupId>
+              <artifactId>onlab-thirdparty</artifactId>
+	      <version>${project.version}</version>
+            </dependency>
+
             <dependency>
                 <groupId>org.onlab.onos</groupId>
                 <artifactId>onos-of-api</artifactId>
diff --git a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java
index c1c56c3..9568f1f 100644
--- a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java
+++ b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java
@@ -161,10 +161,10 @@
         switch (l3m.subtype()) {
         case IP_DST:
             ip = (ModIPInstruction) i;
-            return factory.actions().setNwDst(IPv4Address.of(ip.ip().toRealInt()));
+            return factory.actions().setNwDst(IPv4Address.of(ip.ip().toInt()));
         case IP_SRC:
             ip = (ModIPInstruction) i;
-            return factory.actions().setNwSrc(IPv4Address.of(ip.ip().toRealInt()));
+            return factory.actions().setNwSrc(IPv4Address.of(ip.ip().toInt()));
         default:
             log.warn("Unimplemented action type {}.", l3m.subtype());
             break;
@@ -220,21 +220,21 @@
             case IPV4_DST:
                 ip = (IPCriterion) c;
                 if (ip.ip().isMasked()) {
-                    Masked<IPv4Address> maskedIp = Masked.of(IPv4Address.of(ip.ip().toRealInt()),
-                            IPv4Address.of(ip.ip().netmask().toRealInt()));
+                    Masked<IPv4Address> maskedIp = Masked.of(IPv4Address.of(ip.ip().toInt()),
+                            IPv4Address.of(ip.ip().netmask().toInt()));
                     mBuilder.setMasked(MatchField.IPV4_DST, maskedIp);
                 } else {
-                    mBuilder.setExact(MatchField.IPV4_DST, IPv4Address.of(ip.ip().toRealInt()));
+                    mBuilder.setExact(MatchField.IPV4_DST, IPv4Address.of(ip.ip().toInt()));
                 }
                 break;
             case IPV4_SRC:
                 ip = (IPCriterion) c;
                 if (ip.ip().isMasked()) {
-                    Masked<IPv4Address> maskedIp = Masked.of(IPv4Address.of(ip.ip().toRealInt()),
-                            IPv4Address.of(ip.ip().netmask().toRealInt()));
+                    Masked<IPv4Address> maskedIp = Masked.of(IPv4Address.of(ip.ip().toInt()),
+                            IPv4Address.of(ip.ip().netmask().toInt()));
                     mBuilder.setMasked(MatchField.IPV4_SRC, maskedIp);
                 } else {
-                    mBuilder.setExact(MatchField.IPV4_SRC, IPv4Address.of(ip.ip().toRealInt()));
+                    mBuilder.setExact(MatchField.IPV4_SRC, IPv4Address.of(ip.ip().toInt()));
                 }
                 break;
             case IP_PROTO:
diff --git a/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/LinkDiscovery.java b/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/LinkDiscovery.java
index 0c4502b..bc45f79 100644
--- a/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/LinkDiscovery.java
+++ b/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/LinkDiscovery.java
@@ -339,9 +339,14 @@
             final Iterator<Integer> fastIterator = this.fastPorts.iterator();
             while (fastIterator.hasNext()) {
                 final Integer portNumber = fastIterator.next();
+                OFPortDesc port = findPort(portNumber);
+                if (port == null) {
+                    // port can be null
+                    // #removePort modifies `ports` outside synchronized block
+                    continue;
+                }
                 final int probeCount = this.portProbeCount.get(portNumber)
                         .getAndIncrement();
-                OFPortDesc port = findPort(portNumber);
                 if (probeCount < LinkDiscovery.MAX_PROBE_COUNT) {
                     this.log.debug("sending fast probe to port");
 
diff --git a/tools/build/envDefaults b/tools/build/envDefaults
index f1e1346..9c17212 100644
--- a/tools/build/envDefaults
+++ b/tools/build/envDefaults
@@ -25,6 +25,9 @@
 export ONOS_TAR=$ONOS_STAGE.tar.gz
 
 # Defaults for ONOS testing using remote machines.
+if [ -n "${ONOS_CELL}" -a -f $ONOS_ROOT/tools/test/cells/${ONOS_CELL} ]; then
+   . $ONOS_ROOT/tools/test/cells/${ONOS_CELL}
+fi
 export ONOS_INSTALL_DIR="/opt/onos"     # Installation directory on remote
 export OCI="${OCI:-192.168.56.101}"     # ONOS Controller Instance
 export ONOS_USER="sdn"                  # ONOS user on remote system
diff --git a/tools/dev/bash_profile b/tools/dev/bash_profile
index e14c43b..c7f57a7 100644
--- a/tools/dev/bash_profile
+++ b/tools/dev/bash_profile
@@ -6,7 +6,13 @@
 export ONOS_ROOT=${ONOS_ROOT:-~/onos-next}
 
 # Setup some environmental context for developers
-export JAVA_HOME=${JAVA_HOME:-$(/usr/libexec/java_home -v 1.7)}
+if [ -z "${JAVA_HOME}" ]; then
+    if [ -x /usr/libexec/java_home ]; then
+        export JAVA_HOME=$(/usr/libexec/java_home -v 1.7)
+    elif [ -d /usr/lib/jvm/java-7-openjdk-amd64 ]; then
+        export JAVA_HOME="/usr/lib/jvm/java-7-openjdk-amd64"
+    fi
+fi
 export MAVEN=${MAVEN:-~/Applications/apache-maven-3.2.2}
 export KARAF=${KARAF:-~/Applications/apache-karaf-3.0.1}
 export KARAF_LOG=$KARAF/data/log/karaf.log
@@ -15,7 +21,6 @@
 export PATH="$PATH:$ONOS_ROOT/tools/dev/bin:$ONOS_ROOT/tools/test/bin"
 export PATH="$PATH:$ONOS_ROOT/tools/build"
 export PATH="$PATH:$MAVEN/bin:$KARAF/bin"
-export PATH="$PATH:."
 
 # Convenience utility to warp to various ONOS source projects
 # e.g. 'o api', 'o dev', 'o'
diff --git a/utils/misc/src/main/java/org/onlab/packet/IpAddress.java b/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
index fef0cfd..a440b98 100644
--- a/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
+++ b/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
@@ -2,13 +2,15 @@
 
 import java.util.Arrays;
 
+
+
 /**
  * A class representing an IPv4 address.
  * <p/>
  * TODO this class is a clone of IpPrefix and still needs to be modified to
  * look more like an IpAddress.
  */
-public final class IpAddress {
+public final class IpAddress implements Comparable<IpAddress> {
 
     // TODO a comparator for netmasks? E.g. for sorting by prefix match order.
 
@@ -121,7 +123,7 @@
 
         int mask = DEFAULT_MASK;
         if (parts.length == 2) {
-            mask = Integer.valueOf(parts[1]);
+            mask = Integer.parseInt(parts[1]);
             if (mask > MAX_INET_MASK) {
                 throw new IllegalArgumentException(
                         "Value of subnet mask cannot exceed "
@@ -174,14 +176,6 @@
      * @return the IP address's value as an integer
      */
     public int toInt() {
-        int address = 0;
-        for (int i = 0; i < INET_LEN; i++) {
-            address |= octets[i] << ((INET_LEN - (i + 1)) * 8);
-        }
-        return address;
-    }
-
-    public int toRealInt() {
         int val = 0;
         for (int i = 0; i < octets.length; i++) {
           val <<= 8;
@@ -191,6 +185,15 @@
     }
 
     /**
+     * Converts the IP address to a /32 IP prefix.
+     *
+     * @return the new IP prefix
+     */
+    public IpPrefix toPrefix() {
+        return IpPrefix.valueOf(octets, MAX_INET_MASK);
+    }
+
+    /**
      * Helper for computing the mask value from CIDR.
      *
      * @return an integer bitmask
@@ -280,6 +283,13 @@
     }
 
     @Override
+    public int compareTo(IpAddress o) {
+        Long lv = ((long) this.toInt()) & 0xffffffffL;
+        Long rv = ((long) o.toInt()) & 0xffffffffL;
+        return lv.compareTo(rv);
+    }
+
+    @Override
     public int hashCode() {
         final int prime = 31;
         int result = 1;
diff --git a/utils/misc/src/main/java/org/onlab/packet/IpPrefix.java b/utils/misc/src/main/java/org/onlab/packet/IpPrefix.java
index 84acb82..6e1ebb5 100644
--- a/utils/misc/src/main/java/org/onlab/packet/IpPrefix.java
+++ b/utils/misc/src/main/java/org/onlab/packet/IpPrefix.java
@@ -120,7 +120,7 @@
 
         int mask = DEFAULT_MASK;
         if (parts.length == 2) {
-            mask = Integer.valueOf(parts[1]);
+            mask = Integer.parseInt(parts[1]);
             if (mask > MAX_INET_MASK) {
                 throw new IllegalArgumentException(
                         "Value of subnet mask cannot exceed "
@@ -173,14 +173,6 @@
      * @return the IP address's value as an integer
      */
     public int toInt() {
-        int address = 0;
-        for (int i = 0; i < INET_LEN; i++) {
-            address |= octets[i] << ((INET_LEN - (i + 1)) * 8);
-        }
-        return address;
-    }
-
-    public int toRealInt() {
         int val = 0;
         for (int i = 0; i < octets.length; i++) {
           val <<= 8;
diff --git a/utils/pom.xml b/utils/pom.xml
index feb60e9..df7b97b 100644
--- a/utils/pom.xml
+++ b/utils/pom.xml
@@ -23,6 +23,7 @@
         <module>nio</module>
         <module>osgi</module>
         <module>rest</module>
+	<module>thirdparty</module>
     </modules>
 
     <dependencies>
diff --git a/utils/thirdparty/pom.xml b/utils/thirdparty/pom.xml
new file mode 100644
index 0000000..59ab818
--- /dev/null
+++ b/utils/thirdparty/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.onlab.onos</groupId>
+    <artifactId>onlab-utils</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>onlab-thirdparty</artifactId>
+  <packaging>bundle</packaging>
+
+  <description>ONLab third-party dependencies</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.googlecode.concurrent-trees</groupId>
+      <artifactId>concurrent-trees</artifactId>
+      <version>2.4.0</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>2.3</version>
+        <configuration>
+          <filters>
+            <filter>
+              <artifact>com.googlecode.concurrent-trees:concurrent-trees</artifact>
+              <includes>
+                <include>com/googlecode/**</include>
+              </includes>
+              
+            </filter>
+            <filter>
+              <artifact>com.google.guava:guava</artifact>
+              <excludes>
+                <exclude>**</exclude>
+              </excludes>
+            </filter>
+          </filters>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>            
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <configuration>
+          <instructions>
+            <Export-Package>
+              com.googlecode.concurrenttrees.*
+            </Export-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/utils/thirdparty/src/main/java/org/onlab/thirdparty/OnlabThirdparty.java b/utils/thirdparty/src/main/java/org/onlab/thirdparty/OnlabThirdparty.java
new file mode 100644
index 0000000..df7c48a
--- /dev/null
+++ b/utils/thirdparty/src/main/java/org/onlab/thirdparty/OnlabThirdparty.java
@@ -0,0 +1,11 @@
+package org.onlab.thirdparty;
+
+
+/**
+ * Empty class required to get the onlab-thirdparty module to build properly.
+ * <p/>
+ * TODO Figure out how to remove this.
+ */
+public class OnlabThirdparty {
+
+}