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

Conflicts:
	core/store/hz/net/src/main/java/org/onlab/onos/store/device/impl/DistributedDeviceStore.java
	core/store/hz/net/src/test/java/org/onlab/onos/store/device/impl/DistributedDeviceStoreTest.java
	features/features.xml
	tools/test/cells/office

Change-Id: I08e6d7c6a0bdaae072dd50ff7ac36d94f16d77e1
diff --git a/apps/calendar/pom.xml b/apps/calendar/pom.xml
new file mode 100644
index 0000000..e1c0553
--- /dev/null
+++ b/apps/calendar/pom.xml
@@ -0,0 +1,81 @@
+<?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-apps</artifactId>
+        <version>1.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>onos-app-calendar</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>ONOS simple calendaring REST interface for intents</description>
+
+    <properties>
+        <web.context>/onos/calendar</web.context>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onlab.onos</groupId>
+            <artifactId>onlab-rest</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-servlet</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <version>1.18.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-grizzly2</artifactId>
+            <version>1.18.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <_wab>src/main/webapp/</_wab>
+                        <Bundle-SymbolicName>
+                            ${project.groupId}.${project.artifactId}
+                        </Bundle-SymbolicName>
+                        <Import-Package>
+                            org.osgi.framework,
+                            javax.ws.rs,javax.ws.rs.core,
+                            com.sun.jersey.api.core,
+                            com.sun.jersey.spi.container.servlet,
+                            com.sun.jersey.server.impl.container.servlet,
+                            org.onlab.packet.*,
+                            org.onlab.rest.*,
+                            org.onlab.onos.*
+                        </Import-Package>
+                        <Web-ContextPath>${web.context}</Web-ContextPath>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/apps/calendar/src/main/java/org/onlab/onos/calendar/BandwidthCalendarResource.java b/apps/calendar/src/main/java/org/onlab/onos/calendar/BandwidthCalendarResource.java
new file mode 100644
index 0000000..b05c34e
--- /dev/null
+++ b/apps/calendar/src/main/java/org/onlab/onos/calendar/BandwidthCalendarResource.java
@@ -0,0 +1,43 @@
+package org.onlab.onos.calendar;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.intent.IntentService;
+import org.onlab.rest.BaseResource;
+
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+import static org.onlab.onos.net.PortNumber.portNumber;
+
+/**
+ * Web resource for triggering calendared intents.
+ */
+@Path("intent")
+public class BandwidthCalendarResource extends BaseResource {
+
+    @POST
+    @Path("{src}/{dst}/{srcPort}/{dstPort}/{bandwidth}")
+    public Response createIntent(@PathParam("src") String src,
+                                 @PathParam("dst") String dst,
+                                 @PathParam("srcPort") String srcPort,
+                                 @PathParam("dstPort") String dstPort,
+                                 @PathParam("bandwidth") String bandwidth) {
+        // TODO: implement calls to intent framework
+        IntentService service = get(IntentService.class);
+
+        ConnectPoint srcPoint = new ConnectPoint(deviceId(src), portNumber(srcPort));
+        ConnectPoint dstPoint = new ConnectPoint(deviceId(dst), portNumber(dstPort));
+
+        return Response.ok("Yo! We got src=" + srcPoint + "; dst=" + dstPoint +
+                                   "; bw=" + bandwidth + "; intent service " + service).build();
+    }
+
+    private DeviceId deviceId(String dpid) {
+        return DeviceId.deviceId(URI.create("of:" + dpid));
+    }
+
+}
diff --git a/apps/calendar/src/main/java/org/onlab/onos/calendar/package-info.java b/apps/calendar/src/main/java/org/onlab/onos/calendar/package-info.java
new file mode 100644
index 0000000..a959f3e
--- /dev/null
+++ b/apps/calendar/src/main/java/org/onlab/onos/calendar/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Application providing integration between OSCARS and ONOS intent
+ * framework via REST API.
+ */
+package org.onlab.onos.calendar;
\ No newline at end of file
diff --git a/apps/calendar/src/main/webapp/WEB-INF/web.xml b/apps/calendar/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..7aa6418
--- /dev/null
+++ b/apps/calendar/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         id="ONOS" version="2.5">
+    <display-name>ONOS GUI</display-name>
+
+    <servlet>
+        <servlet-name>JAX-RS Service</servlet-name>
+        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
+        <init-param>
+            <param-name>com.sun.jersey.config.property.packages</param-name>
+            <param-value>org.onlab.onos.calendar</param-value>
+        </init-param>
+        <load-on-startup>10</load-on-startup>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>JAX-RS Service</servlet-name>
+        <url-pattern>/rs/*</url-pattern>
+    </servlet-mapping>
+
+</web-app>
\ No newline at end of file
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/pom.xml b/apps/pom.xml
index eeff7b4..e9870ea 100644
--- a/apps/pom.xml
+++ b/apps/pom.xml
@@ -25,6 +25,7 @@
         <module>proxyarp</module>
         <module>config</module>
         <module>sdnip</module>
+        <module>calendar</module>
     </modules>
 
     <properties>
diff --git a/apps/sdnip/pom.xml b/apps/sdnip/pom.xml
index c8db20d..ce77bf7 100644
--- a/apps/sdnip/pom.xml
+++ b/apps/sdnip/pom.xml
@@ -36,6 +36,36 @@
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
     </dependency>
+
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onlab-thirdparty</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onlab-misc</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onos-cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.karaf.shell</groupId>
+      <artifactId>org.apache.karaf.shell.console</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.core</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.easymock</groupId>
+      <artifactId>easymock</artifactId>
+      <scope>test</scope>
+    </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..dc29285
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
@@ -0,0 +1,767 @@
+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 radix tree.
+    // The key in this tree is the binary string of prefix of the route.
+    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 hostService the host 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 the radix tree. If there was an existing
+     * next hop 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 radix tree, and if successful, then try to delete
+     * the related 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 radix tree
+         // while we're pushing intents. If the tree changes, the
+         // tree and intents 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
+                    // radix tree 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"
+                            + " the radix tree", 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..e7ed601 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
@@ -2,21 +2,30 @@
 
 import static org.slf4j.LoggerFactory.getLogger;
 
+import java.util.Collection;
+
 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.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
 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.BgpRouteEntry;
+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;
 
 /**
- * Placeholder SDN-IP component.
+ * Component for the SDN-IP peering application.
  */
 @Component(immediate = true)
-public class SdnIp {
+@Service
+public class SdnIp implements SdnIpService {
 
     private final Logger log = getLogger(getClass());
 
@@ -28,6 +37,8 @@
 
     private SdnIpConfigReader config;
     private PeerConnectivity peerConnectivity;
+    private Router router;
+    private BgpSessionManager bgpSessionManager;
 
     @Activate
     protected void activate() {
@@ -41,10 +52,31 @@
         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
     protected void deactivate() {
         log.info("Stopped");
     }
+
+    @Override
+    public Collection<BgpRouteEntry> getBgpRoutes() {
+        return bgpSessionManager.getBgpRoutes();
+    }
+
+    @Override
+    public Collection<RouteEntry> getRoutes() {
+        return router.getRoutes();
+    }
 }
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIpService.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIpService.java
new file mode 100644
index 0000000..187ebb2
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIpService.java
@@ -0,0 +1,24 @@
+package org.onlab.onos.sdnip;
+
+import java.util.Collection;
+
+import org.onlab.onos.sdnip.bgp.BgpRouteEntry;
+
+/**
+ * Service interface exported by SDN-IP.
+ */
+public interface SdnIpService {
+    /**
+     * Gets the BGP routes.
+     *
+     * @return the BGP routes
+     */
+    public Collection<BgpRouteEntry> getBgpRoutes();
+
+    /**
+     * Gets all the routes known to SDN-IP.
+     *
+     * @return the SDN-IP routes
+     */
+    public Collection<RouteEntry> getRoutes();
+}
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/main/java/org/onlab/onos/sdnip/cli/BgpRoutesListCommand.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/BgpRoutesListCommand.java
new file mode 100644
index 0000000..63cc305
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/BgpRoutesListCommand.java
@@ -0,0 +1,57 @@
+package org.onlab.onos.sdnip.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.onos.cli.AbstractShellCommand;
+import org.onlab.onos.sdnip.SdnIpService;
+import org.onlab.onos.sdnip.bgp.BgpConstants;
+import org.onlab.onos.sdnip.bgp.BgpRouteEntry;
+
+/**
+ * Command to show the routes learned through BGP.
+ */
+@Command(scope = "onos", name = "bgp-routes",
+         description = "Lists all routes received from BGP")
+public class BgpRoutesListCommand extends AbstractShellCommand {
+
+    private static final String FORMAT =
+            "prefix=%s, nexthop=%s, origin=%s, localpref=%s, med=%s, aspath=%s, bgpid=%s";
+
+    @Override
+    protected void execute() {
+        SdnIpService service = get(SdnIpService.class);
+
+        for (BgpRouteEntry route : service.getBgpRoutes()) {
+            printRoute(route);
+        }
+    }
+
+    private void printRoute(BgpRouteEntry route) {
+        if (route != null) {
+            print(FORMAT, route.prefix(), route.nextHop(),
+                    originToString(route.getOrigin()), route.getLocalPref(),
+                    route.getMultiExitDisc(), route.getAsPath(),
+                    route.getBgpSession().getRemoteBgpId());
+        }
+    }
+
+    private static String originToString(int origin) {
+        String originString = "UNKNOWN";
+
+        switch (origin) {
+        case BgpConstants.Update.Origin.IGP:
+            originString = "IGP";
+            break;
+        case BgpConstants.Update.Origin.EGP:
+            originString = "EGP";
+            break;
+        case BgpConstants.Update.Origin.INCOMPLETE:
+            originString = "INCOMPLETE";
+            break;
+        default:
+            break;
+        }
+
+        return originString;
+    }
+
+}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/RoutesListCommand.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/RoutesListCommand.java
new file mode 100644
index 0000000..1af409a
--- /dev/null
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/RoutesListCommand.java
@@ -0,0 +1,32 @@
+package org.onlab.onos.sdnip.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.onos.cli.AbstractShellCommand;
+import org.onlab.onos.sdnip.RouteEntry;
+import org.onlab.onos.sdnip.SdnIpService;
+
+/**
+ * Command to show the list of routes in SDN-IP's routing table.
+ */
+@Command(scope = "onos", name = "routes",
+        description = "Lists all routes known to SDN-IP")
+public class RoutesListCommand extends AbstractShellCommand {
+
+    private static final String FORMAT =
+            "prefix=%s, nexthop=%s";
+
+    @Override
+    protected void execute() {
+        SdnIpService service = get(SdnIpService.class);
+
+        for (RouteEntry route : service.getRoutes()) {
+            printRoute(route);
+        }
+    }
+
+    private void printRoute(RouteEntry route) {
+        if (route != null) {
+            print(FORMAT, route.prefix(), route.nextHop());
+        }
+    }
+}
diff --git a/apps/sdnip/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/sdnip/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..62fccec
--- /dev/null
+++ b/apps/sdnip/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,11 @@
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+
+  <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
+    <command>
+      <action class="org.onlab.onos.sdnip.cli.BgpRoutesListCommand"/>
+    </command>
+    <command>
+      <action class="org.onlab.onos.sdnip.cli.RoutesListCommand"/>
+    </command>
+  </command-bundle>
+</blueprint>
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/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/AsPathTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/AsPathTest.java
new file mode 100644
index 0000000..c14c20b
--- /dev/null
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/AsPathTest.java
@@ -0,0 +1,169 @@
+package org.onlab.onos.sdnip.bgp;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for the BgpRouteEntry.AsPath class.
+ */
+public class AsPathTest {
+    /**
+     * Generates an AS Path.
+     *
+     * @return a generated AS Path
+     */
+    private BgpRouteEntry.AsPath generateAsPath() {
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        byte pathSegmentType1 = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers1 = new ArrayList<>();
+        segmentAsNumbers1.add((long) 1);
+        segmentAsNumbers1.add((long) 2);
+        segmentAsNumbers1.add((long) 3);
+        BgpRouteEntry.PathSegment pathSegment1 =
+            new BgpRouteEntry.PathSegment(pathSegmentType1, segmentAsNumbers1);
+        pathSegments.add(pathSegment1);
+        //
+        byte pathSegmentType2 = (byte) BgpConstants.Update.AsPath.AS_SET;
+        ArrayList<Long> segmentAsNumbers2 = new ArrayList<>();
+        segmentAsNumbers2.add((long) 4);
+        segmentAsNumbers2.add((long) 5);
+        segmentAsNumbers2.add((long) 6);
+        BgpRouteEntry.PathSegment pathSegment2 =
+            new BgpRouteEntry.PathSegment(pathSegmentType2, segmentAsNumbers2);
+        pathSegments.add(pathSegment2);
+        //
+        BgpRouteEntry.AsPath asPath = new BgpRouteEntry.AsPath(pathSegments);
+
+        return asPath;
+    }
+
+    /**
+     * Tests valid class constructor.
+     */
+    @Test
+    public void testConstructor() {
+        BgpRouteEntry.AsPath asPath = generateAsPath();
+
+        String expectedString =
+            "AsPath{pathSegments=" +
+            "[PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}, " +
+            "PathSegment{type=1, segmentAsNumbers=[4, 5, 6]}]}";
+        assertThat(asPath.toString(), is(expectedString));
+    }
+
+    /**
+     * Tests invalid class constructor for null Path Segments.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullPathSegments() {
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = null;
+        new BgpRouteEntry.AsPath(pathSegments);
+    }
+
+    /**
+     * Tests getting the fields of an AS Path.
+     */
+    @Test
+    public void testGetFields() {
+        // Create the fields to compare against
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        byte pathSegmentType1 = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers1 = new ArrayList<>();
+        segmentAsNumbers1.add((long) 1);
+        segmentAsNumbers1.add((long) 2);
+        segmentAsNumbers1.add((long) 3);
+        BgpRouteEntry.PathSegment pathSegment1 =
+            new BgpRouteEntry.PathSegment(pathSegmentType1, segmentAsNumbers1);
+        pathSegments.add(pathSegment1);
+        //
+        byte pathSegmentType2 = (byte) BgpConstants.Update.AsPath.AS_SET;
+        ArrayList<Long> segmentAsNumbers2 = new ArrayList<>();
+        segmentAsNumbers2.add((long) 4);
+        segmentAsNumbers2.add((long) 5);
+        segmentAsNumbers2.add((long) 6);
+        BgpRouteEntry.PathSegment pathSegment2 =
+            new BgpRouteEntry.PathSegment(pathSegmentType2, segmentAsNumbers2);
+        pathSegments.add(pathSegment2);
+
+        // Generate the entry to test
+        BgpRouteEntry.AsPath asPath = generateAsPath();
+
+        assertThat(asPath.getPathSegments(), is(pathSegments));
+    }
+
+    /**
+     * Tests getting the AS Path Length.
+     */
+    @Test
+    public void testGetAsPathLength() {
+        BgpRouteEntry.AsPath asPath = generateAsPath();
+        assertThat(asPath.getAsPathLength(), is(4));
+
+        // Create an empty AS Path
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        asPath = new BgpRouteEntry.AsPath(pathSegments);
+        assertThat(asPath.getAsPathLength(), is(0));
+    }
+
+    /**
+     * Tests equality of {@link BgpRouteEntry.AsPath}.
+     */
+    @Test
+    public void testEquality() {
+        BgpRouteEntry.AsPath asPath1 = generateAsPath();
+        BgpRouteEntry.AsPath asPath2 = generateAsPath();
+
+        assertThat(asPath1, is(asPath2));
+    }
+
+    /**
+     * Tests non-equality of {@link BgpRouteEntry.AsPath}.
+     */
+    @Test
+    public void testNonEquality() {
+        BgpRouteEntry.AsPath asPath1 = generateAsPath();
+
+        // Setup AS Path 2
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        byte pathSegmentType1 = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers1 = new ArrayList<>();
+        segmentAsNumbers1.add((long) 1);
+        segmentAsNumbers1.add((long) 2);
+        segmentAsNumbers1.add((long) 3);
+        BgpRouteEntry.PathSegment pathSegment1 =
+            new BgpRouteEntry.PathSegment(pathSegmentType1, segmentAsNumbers1);
+        pathSegments.add(pathSegment1);
+        //
+        byte pathSegmentType2 = (byte) BgpConstants.Update.AsPath.AS_SET;
+        ArrayList<Long> segmentAsNumbers2 = new ArrayList<>();
+        segmentAsNumbers2.add((long) 4);
+        segmentAsNumbers2.add((long) 55);                       // Different
+        segmentAsNumbers2.add((long) 6);
+        BgpRouteEntry.PathSegment pathSegment2 =
+            new BgpRouteEntry.PathSegment(pathSegmentType2, segmentAsNumbers2);
+        pathSegments.add(pathSegment2);
+        //
+        BgpRouteEntry.AsPath asPath2 = new BgpRouteEntry.AsPath(pathSegments);
+
+        assertThat(asPath1, is(not(asPath2)));
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        BgpRouteEntry.AsPath asPath = generateAsPath();
+
+        String expectedString =
+            "AsPath{pathSegments=" +
+            "[PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}, " +
+            "PathSegment{type=1, segmentAsNumbers=[4, 5, 6]}]}";
+        assertThat(asPath.toString(), is(expectedString));
+    }
+}
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpRouteEntryTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpRouteEntryTest.java
new file mode 100644
index 0000000..52a0214
--- /dev/null
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpRouteEntryTest.java
@@ -0,0 +1,498 @@
+package org.onlab.onos.sdnip.bgp;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+
+/**
+ * Unit tests for the BgpRouteEntry class.
+ */
+public class BgpRouteEntryTest {
+    private BgpSession bgpSession;
+    private static final IpAddress BGP_SESSION_BGP_ID =
+        IpAddress.valueOf("10.0.0.1");
+    private static final IpAddress BGP_SESSION_IP_ADDRESS =
+        IpAddress.valueOf("20.0.0.1");
+
+    private BgpSession bgpSession2;
+    private static final IpAddress BGP_SESSION_BGP_ID2 =
+        IpAddress.valueOf("10.0.0.2");
+    private static final IpAddress BGP_SESSION_IP_ADDRESS2 =
+        IpAddress.valueOf("20.0.0.1");
+
+    private BgpSession bgpSession3;
+    private static final IpAddress BGP_SESSION_BGP_ID3 =
+        IpAddress.valueOf("10.0.0.1");
+    private static final IpAddress BGP_SESSION_IP_ADDRESS3 =
+        IpAddress.valueOf("20.0.0.2");
+
+    @Before
+    public void setUp() throws Exception {
+        // Mock objects for testing
+        bgpSession = createMock(BgpSession.class);
+        bgpSession2 = createMock(BgpSession.class);
+        bgpSession3 = createMock(BgpSession.class);
+
+        // Setup the BGP Sessions
+        expect(bgpSession.getRemoteBgpId())
+            .andReturn(BGP_SESSION_BGP_ID).anyTimes();
+        expect(bgpSession.getRemoteIp4Address())
+            .andReturn(BGP_SESSION_IP_ADDRESS).anyTimes();
+        //
+        expect(bgpSession2.getRemoteBgpId())
+            .andReturn(BGP_SESSION_BGP_ID2).anyTimes();
+        expect(bgpSession2.getRemoteIp4Address())
+            .andReturn(BGP_SESSION_IP_ADDRESS2).anyTimes();
+        //
+        expect(bgpSession3.getRemoteBgpId())
+            .andReturn(BGP_SESSION_BGP_ID3).anyTimes();
+        expect(bgpSession3.getRemoteIp4Address())
+            .andReturn(BGP_SESSION_IP_ADDRESS3).anyTimes();
+
+        replay(bgpSession);
+        replay(bgpSession2);
+        replay(bgpSession3);
+    }
+
+    /**
+     * Generates a BGP Route Entry.
+     *
+     * @return a generated BGP Route Entry
+     */
+    private BgpRouteEntry generateBgpRouteEntry() {
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+        byte origin = BgpConstants.Update.Origin.IGP;
+        // Setup the AS Path
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        byte pathSegmentType1 = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers1 = new ArrayList<>();
+        segmentAsNumbers1.add((long) 1);
+        segmentAsNumbers1.add((long) 2);
+        segmentAsNumbers1.add((long) 3);
+        BgpRouteEntry.PathSegment pathSegment1 =
+            new BgpRouteEntry.PathSegment(pathSegmentType1, segmentAsNumbers1);
+        pathSegments.add(pathSegment1);
+        //
+        byte pathSegmentType2 = (byte) BgpConstants.Update.AsPath.AS_SET;
+        ArrayList<Long> segmentAsNumbers2 = new ArrayList<>();
+        segmentAsNumbers2.add((long) 4);
+        segmentAsNumbers2.add((long) 5);
+        segmentAsNumbers2.add((long) 6);
+        BgpRouteEntry.PathSegment pathSegment2 =
+            new BgpRouteEntry.PathSegment(pathSegmentType2, segmentAsNumbers2);
+        pathSegments.add(pathSegment2);
+        //
+        BgpRouteEntry.AsPath asPath = new BgpRouteEntry.AsPath(pathSegments);
+        //
+        long localPref = 100;
+        long multiExitDisc = 20;
+
+        BgpRouteEntry bgpRouteEntry =
+            new BgpRouteEntry(bgpSession, prefix, nextHop, origin, asPath,
+                              localPref);
+        bgpRouteEntry.setMultiExitDisc(multiExitDisc);
+
+        return bgpRouteEntry;
+    }
+
+    /**
+     * Tests valid class constructor.
+     */
+    @Test
+    public void testConstructor() {
+        BgpRouteEntry bgpRouteEntry = generateBgpRouteEntry();
+
+        String expectedString =
+            "BgpRouteEntry{prefix=1.2.3.0/24, nextHop=5.6.7.8, " +
+            "bgpId=10.0.0.1, origin=0, asPath=AsPath{pathSegments=" +
+            "[PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}, " +
+            "PathSegment{type=1, segmentAsNumbers=[4, 5, 6]}]}, " +
+            "localPref=100, multiExitDisc=20}";
+        assertThat(bgpRouteEntry.toString(), is(expectedString));
+    }
+
+    /**
+     * Tests invalid class constructor for null BGP Session.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullBgpSession() {
+        BgpSession bgpSessionNull = null;
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+        byte origin = BgpConstants.Update.Origin.IGP;
+        // Setup the AS Path
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        BgpRouteEntry.AsPath asPath = new BgpRouteEntry.AsPath(pathSegments);
+        //
+        long localPref = 100;
+
+        new BgpRouteEntry(bgpSessionNull, prefix, nextHop, origin, asPath,
+                    localPref);
+    }
+
+    /**
+     * Tests invalid class constructor for null AS Path.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullAsPath() {
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+        byte origin = BgpConstants.Update.Origin.IGP;
+        BgpRouteEntry.AsPath asPath = null;
+        long localPref = 100;
+
+        new BgpRouteEntry(bgpSession, prefix, nextHop, origin, asPath,
+                    localPref);
+    }
+
+    /**
+     * Tests getting the fields of a BGP route entry.
+     */
+    @Test
+    public void testGetFields() {
+        // Create the fields to compare against
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+        byte origin = BgpConstants.Update.Origin.IGP;
+        // Setup the AS Path
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        byte pathSegmentType1 = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers1 = new ArrayList<>();
+        segmentAsNumbers1.add((long) 1);
+        segmentAsNumbers1.add((long) 2);
+        segmentAsNumbers1.add((long) 3);
+        BgpRouteEntry.PathSegment pathSegment1 =
+            new BgpRouteEntry.PathSegment(pathSegmentType1, segmentAsNumbers1);
+        pathSegments.add(pathSegment1);
+        //
+        byte pathSegmentType2 = (byte) BgpConstants.Update.AsPath.AS_SET;
+        ArrayList<Long> segmentAsNumbers2 = new ArrayList<>();
+        segmentAsNumbers2.add((long) 4);
+        segmentAsNumbers2.add((long) 5);
+        segmentAsNumbers2.add((long) 6);
+        BgpRouteEntry.PathSegment pathSegment2 =
+            new BgpRouteEntry.PathSegment(pathSegmentType2, segmentAsNumbers2);
+        pathSegments.add(pathSegment2);
+        //
+        BgpRouteEntry.AsPath asPath = new BgpRouteEntry.AsPath(pathSegments);
+        //
+        long localPref = 100;
+        long multiExitDisc = 20;
+
+        // Generate the entry to test
+        BgpRouteEntry bgpRouteEntry = generateBgpRouteEntry();
+
+        assertThat(bgpRouteEntry.prefix(), is(prefix));
+        assertThat(bgpRouteEntry.nextHop(), is(nextHop));
+        assertThat(bgpRouteEntry.getBgpSession(), is(bgpSession));
+        assertThat(bgpRouteEntry.getOrigin(), is(origin));
+        assertThat(bgpRouteEntry.getAsPath(), is(asPath));
+        assertThat(bgpRouteEntry.getLocalPref(), is(localPref));
+        assertThat(bgpRouteEntry.getMultiExitDisc(), is(multiExitDisc));
+    }
+
+    /**
+     * Tests whether a BGP route entry is a local route.
+     */
+    @Test
+    public void testIsLocalRoute() {
+        //
+        // Test non-local route
+        //
+        BgpRouteEntry bgpRouteEntry = generateBgpRouteEntry();
+        assertThat(bgpRouteEntry.isLocalRoute(), is(false));
+
+        //
+        // Test local route with AS Path that begins with AS_SET
+        //
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+        byte origin = BgpConstants.Update.Origin.IGP;
+        // Setup the AS Path
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        byte pathSegmentType1 = (byte) BgpConstants.Update.AsPath.AS_SET;
+        ArrayList<Long> segmentAsNumbers1 = new ArrayList<>();
+        segmentAsNumbers1.add((long) 1);
+        segmentAsNumbers1.add((long) 2);
+        segmentAsNumbers1.add((long) 3);
+        BgpRouteEntry.PathSegment pathSegment1 =
+            new BgpRouteEntry.PathSegment(pathSegmentType1, segmentAsNumbers1);
+        pathSegments.add(pathSegment1);
+        //
+        byte pathSegmentType2 = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers2 = new ArrayList<>();
+        segmentAsNumbers2.add((long) 4);
+        segmentAsNumbers2.add((long) 5);
+        segmentAsNumbers2.add((long) 6);
+        BgpRouteEntry.PathSegment pathSegment2 =
+            new BgpRouteEntry.PathSegment(pathSegmentType2, segmentAsNumbers2);
+        pathSegments.add(pathSegment2);
+        //
+        BgpRouteEntry.AsPath asPath = new BgpRouteEntry.AsPath(pathSegments);
+        //
+        long localPref = 100;
+        long multiExitDisc = 20;
+        //
+        bgpRouteEntry =
+            new BgpRouteEntry(bgpSession, prefix, nextHop, origin, asPath,
+                              localPref);
+        bgpRouteEntry.setMultiExitDisc(multiExitDisc);
+        assertThat(bgpRouteEntry.isLocalRoute(), is(true));
+
+        //
+        // Test local route with empty AS Path
+        //
+        pathSegments = new ArrayList<>();
+        asPath = new BgpRouteEntry.AsPath(pathSegments);
+        bgpRouteEntry =
+            new BgpRouteEntry(bgpSession, prefix, nextHop, origin, asPath,
+                              localPref);
+        bgpRouteEntry.setMultiExitDisc(multiExitDisc);
+        assertThat(bgpRouteEntry.isLocalRoute(), is(true));
+    }
+
+    /**
+     * Tests getting the BGP Neighbor AS number for a route.
+     */
+    @Test
+    public void testGetNeighborAs() {
+        //
+        // Get neighbor AS for non-local route
+        //
+        BgpRouteEntry bgpRouteEntry = generateBgpRouteEntry();
+        assertThat(bgpRouteEntry.getNeighborAs(), is((long) 1));
+
+        //
+        // Get neighbor AS for a local route
+        //
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+        byte origin = BgpConstants.Update.Origin.IGP;
+        // Setup the AS Path
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        BgpRouteEntry.AsPath asPath = new BgpRouteEntry.AsPath(pathSegments);
+        //
+        long localPref = 100;
+        long multiExitDisc = 20;
+        //
+        bgpRouteEntry =
+            new BgpRouteEntry(bgpSession, prefix, nextHop, origin, asPath,
+                              localPref);
+        bgpRouteEntry.setMultiExitDisc(multiExitDisc);
+        assertThat(bgpRouteEntry.getNeighborAs(), is(BgpConstants.BGP_AS_0));
+    }
+
+    /**
+     * Tests whether a BGP route entry has AS Path loop.
+     */
+    @Test
+    public void testHasAsPathLoop() {
+        BgpRouteEntry bgpRouteEntry = generateBgpRouteEntry();
+
+        // Test for loops: test each AS number in the interval [1, 6]
+        for (int i = 1; i <= 6; i++) {
+            assertThat(bgpRouteEntry.hasAsPathLoop(i), is(true));
+        }
+
+        // Test for non-loops
+        assertThat(bgpRouteEntry.hasAsPathLoop(500), is(false));
+    }
+
+    /**
+     * Tests the BGP Decision Process comparison of BGP routes.
+     */
+    @Test
+    public void testBgpDecisionProcessComparison() {
+        BgpRouteEntry bgpRouteEntry1 = generateBgpRouteEntry();
+        BgpRouteEntry bgpRouteEntry2 = generateBgpRouteEntry();
+
+        //
+        // Compare two routes that are same
+        //
+        assertThat(bgpRouteEntry1.isBetterThan(bgpRouteEntry2), is(true));
+        assertThat(bgpRouteEntry2.isBetterThan(bgpRouteEntry1), is(true));
+
+        //
+        // Compare two routes with different LOCAL_PREF
+        //
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+        byte origin = BgpConstants.Update.Origin.IGP;
+        // Setup the AS Path
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        byte pathSegmentType1 = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers1 = new ArrayList<>();
+        segmentAsNumbers1.add((long) 1);
+        segmentAsNumbers1.add((long) 2);
+        segmentAsNumbers1.add((long) 3);
+        BgpRouteEntry.PathSegment pathSegment1 =
+            new BgpRouteEntry.PathSegment(pathSegmentType1, segmentAsNumbers1);
+        pathSegments.add(pathSegment1);
+        //
+        byte pathSegmentType2 = (byte) BgpConstants.Update.AsPath.AS_SET;
+        ArrayList<Long> segmentAsNumbers2 = new ArrayList<>();
+        segmentAsNumbers2.add((long) 4);
+        segmentAsNumbers2.add((long) 5);
+        segmentAsNumbers2.add((long) 6);
+        BgpRouteEntry.PathSegment pathSegment2 =
+            new BgpRouteEntry.PathSegment(pathSegmentType2, segmentAsNumbers2);
+        pathSegments.add(pathSegment2);
+        //
+        BgpRouteEntry.AsPath asPath = new BgpRouteEntry.AsPath(pathSegments);
+        //
+        long localPref = 50;                                    // Different
+        long multiExitDisc = 20;
+        bgpRouteEntry2 =
+            new BgpRouteEntry(bgpSession, prefix, nextHop, origin, asPath,
+                              localPref);
+        bgpRouteEntry2.setMultiExitDisc(multiExitDisc);
+        //
+        assertThat(bgpRouteEntry1.isBetterThan(bgpRouteEntry2), is(true));
+        assertThat(bgpRouteEntry2.isBetterThan(bgpRouteEntry1), is(false));
+        localPref = bgpRouteEntry1.getLocalPref();              // Restore
+
+        //
+        // Compare two routes with different AS_PATH length
+        //
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments2 = new ArrayList<>();
+        pathSegments2.add(pathSegment1);
+        // Different AS Path
+        BgpRouteEntry.AsPath asPath2 = new BgpRouteEntry.AsPath(pathSegments2);
+        bgpRouteEntry2 =
+            new BgpRouteEntry(bgpSession, prefix, nextHop, origin, asPath2,
+                              localPref);
+        bgpRouteEntry2.setMultiExitDisc(multiExitDisc);
+        //
+        assertThat(bgpRouteEntry1.isBetterThan(bgpRouteEntry2), is(false));
+        assertThat(bgpRouteEntry2.isBetterThan(bgpRouteEntry1), is(true));
+
+        //
+        // Compare two routes with different ORIGIN
+        //
+        origin = BgpConstants.Update.Origin.EGP;                // Different
+        bgpRouteEntry2 =
+            new BgpRouteEntry(bgpSession, prefix, nextHop, origin, asPath,
+                              localPref);
+        bgpRouteEntry2.setMultiExitDisc(multiExitDisc);
+        //
+        assertThat(bgpRouteEntry1.isBetterThan(bgpRouteEntry2), is(true));
+        assertThat(bgpRouteEntry2.isBetterThan(bgpRouteEntry1), is(false));
+        origin = bgpRouteEntry1.getOrigin();                    // Restore
+
+        //
+        // Compare two routes with different MULTI_EXIT_DISC
+        //
+        multiExitDisc = 10;                                     // Different
+        bgpRouteEntry2 =
+            new BgpRouteEntry(bgpSession, prefix, nextHop, origin, asPath,
+                              localPref);
+        bgpRouteEntry2.setMultiExitDisc(multiExitDisc);
+        //
+        assertThat(bgpRouteEntry1.isBetterThan(bgpRouteEntry2), is(true));
+        assertThat(bgpRouteEntry2.isBetterThan(bgpRouteEntry1), is(false));
+        multiExitDisc = bgpRouteEntry1.getMultiExitDisc();      // Restore
+
+        //
+        // Compare two routes with different BGP ID
+        //
+        bgpRouteEntry2 =
+            new BgpRouteEntry(bgpSession2, prefix, nextHop, origin, asPath,
+                              localPref);
+        bgpRouteEntry2.setMultiExitDisc(multiExitDisc);
+        //
+        assertThat(bgpRouteEntry1.isBetterThan(bgpRouteEntry2), is(true));
+        assertThat(bgpRouteEntry2.isBetterThan(bgpRouteEntry1), is(false));
+
+        //
+        // Compare two routes with different BGP address
+        //
+        bgpRouteEntry2 =
+            new BgpRouteEntry(bgpSession3, prefix, nextHop, origin, asPath,
+                              localPref);
+        bgpRouteEntry2.setMultiExitDisc(multiExitDisc);
+        //
+        assertThat(bgpRouteEntry1.isBetterThan(bgpRouteEntry2), is(true));
+        assertThat(bgpRouteEntry2.isBetterThan(bgpRouteEntry1), is(false));
+    }
+
+    /**
+     * Tests equality of {@link BgpRouteEntry}.
+     */
+    @Test
+    public void testEquality() {
+        BgpRouteEntry bgpRouteEntry1 = generateBgpRouteEntry();
+        BgpRouteEntry bgpRouteEntry2 = generateBgpRouteEntry();
+
+        assertThat(bgpRouteEntry1, is(bgpRouteEntry2));
+    }
+
+    /**
+     * Tests non-equality of {@link BgpRouteEntry}.
+     */
+    @Test
+    public void testNonEquality() {
+        BgpRouteEntry bgpRouteEntry1 = generateBgpRouteEntry();
+
+        // Setup BGP Route 2
+        IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
+        IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
+        byte origin = BgpConstants.Update.Origin.IGP;
+        // Setup the AS Path
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        byte pathSegmentType1 = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers1 = new ArrayList<>();
+        segmentAsNumbers1.add((long) 1);
+        segmentAsNumbers1.add((long) 2);
+        segmentAsNumbers1.add((long) 3);
+        BgpRouteEntry.PathSegment pathSegment1 =
+            new BgpRouteEntry.PathSegment(pathSegmentType1, segmentAsNumbers1);
+        pathSegments.add(pathSegment1);
+        //
+        byte pathSegmentType2 = (byte) BgpConstants.Update.AsPath.AS_SET;
+        ArrayList<Long> segmentAsNumbers2 = new ArrayList<>();
+        segmentAsNumbers2.add((long) 4);
+        segmentAsNumbers2.add((long) 5);
+        segmentAsNumbers2.add((long) 6);
+        BgpRouteEntry.PathSegment pathSegment2 =
+            new BgpRouteEntry.PathSegment(pathSegmentType2, segmentAsNumbers2);
+        pathSegments.add(pathSegment2);
+        //
+        BgpRouteEntry.AsPath asPath = new BgpRouteEntry.AsPath(pathSegments);
+        //
+        long localPref = 500;                                   // Different
+        long multiExitDisc = 20;
+        BgpRouteEntry bgpRouteEntry2 =
+            new BgpRouteEntry(bgpSession, prefix, nextHop, origin, asPath,
+                              localPref);
+        bgpRouteEntry2.setMultiExitDisc(multiExitDisc);
+
+        assertThat(bgpRouteEntry1, is(not(bgpRouteEntry2)));
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        BgpRouteEntry bgpRouteEntry = generateBgpRouteEntry();
+
+        String expectedString =
+            "BgpRouteEntry{prefix=1.2.3.0/24, nextHop=5.6.7.8, " +
+            "bgpId=10.0.0.1, origin=0, asPath=AsPath{pathSegments=" +
+            "[PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}, " +
+            "PathSegment{type=1, segmentAsNumbers=[4, 5, 6]}]}, " +
+            "localPref=100, multiExitDisc=20}";
+        assertThat(bgpRouteEntry.toString(), is(expectedString));
+    }
+}
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpSessionManagerTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpSessionManagerTest.java
new file mode 100644
index 0000000..99e3be1
--- /dev/null
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpSessionManagerTest.java
@@ -0,0 +1,403 @@
+package org.onlab.onos.sdnip.bgp;
+
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.Assert.assertThat;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.jboss.netty.bootstrap.ClientBootstrap;
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.Channel;
+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.NioClientSocketChannelFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.onos.sdnip.RouteListener;
+import org.onlab.onos.sdnip.RouteUpdate;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.util.TestUtils;
+import org.onlab.util.TestUtils.TestUtilsException;
+
+import com.google.common.net.InetAddresses;
+
+/**
+ * Unit tests for the BgpSessionManager class.
+ */
+public class BgpSessionManagerTest {
+    private static final IpAddress IP_LOOPBACK_ID =
+        IpAddress.valueOf("127.0.0.1");
+    private static final IpAddress BGP_PEER1_ID = IpAddress.valueOf("10.0.0.1");
+    private static final long DEFAULT_LOCAL_PREF = 10;
+    private static final long DEFAULT_MULTI_EXIT_DISC = 20;
+
+    // The BGP Session Manager to test
+    private BgpSessionManager bgpSessionManager;
+
+    // Remote Peer state
+    private ClientBootstrap peerBootstrap;
+    private TestBgpPeerChannelHandler peerChannelHandler =
+        new TestBgpPeerChannelHandler(BGP_PEER1_ID, DEFAULT_LOCAL_PREF);
+    private TestBgpPeerFrameDecoder peerFrameDecoder =
+        new TestBgpPeerFrameDecoder();
+
+    // The socket that the Remote Peer should connect to
+    private InetSocketAddress connectToSocket;
+
+    private final DummyRouteListener dummyRouteListener =
+        new DummyRouteListener();
+
+    /**
+     * Dummy implementation for the RouteListener interface.
+     */
+    private class DummyRouteListener implements RouteListener {
+        @Override
+        public void update(RouteUpdate routeUpdate) {
+            // Nothing to do
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        //
+        // Setup the BGP Session Manager to test, and start listening for BGP
+        // connections.
+        //
+        bgpSessionManager = new BgpSessionManager(dummyRouteListener);
+        // NOTE: We use port 0 to bind on any available port
+        bgpSessionManager.startUp(0);
+
+        // Get the port number the BGP Session Manager is listening on
+        Channel serverChannel = TestUtils.getField(bgpSessionManager,
+                                                   "serverChannel");
+        SocketAddress socketAddress = serverChannel.getLocalAddress();
+        InetSocketAddress inetSocketAddress =
+            (InetSocketAddress) socketAddress;
+
+        //
+        // Setup the BGP Peer, i.e., the "remote" BGP router that will
+        // initiate the BGP connection, send BGP UPDATE messages, etc.
+        //
+        ChannelFactory channelFactory =
+            new NioClientSocketChannelFactory(Executors.newCachedThreadPool(),
+                                              Executors.newCachedThreadPool());
+        ChannelPipelineFactory pipelineFactory = new ChannelPipelineFactory() {
+                @Override
+                public ChannelPipeline getPipeline() throws Exception {
+                    // Setup the transmitting pipeline
+                    ChannelPipeline pipeline = Channels.pipeline();
+                    pipeline.addLast("TestBgpPeerFrameDecoder",
+                                     peerFrameDecoder);
+                    pipeline.addLast("TestBgpPeerChannelHandler",
+                                     peerChannelHandler);
+                    return pipeline;
+                }
+            };
+
+        peerBootstrap = new ClientBootstrap(channelFactory);
+        peerBootstrap.setOption("child.keepAlive", true);
+        peerBootstrap.setOption("child.tcpNoDelay", true);
+        peerBootstrap.setPipelineFactory(pipelineFactory);
+
+        InetAddress connectToAddress = InetAddresses.forString("127.0.0.1");
+        connectToSocket = new InetSocketAddress(connectToAddress,
+                                                inetSocketAddress.getPort());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        bgpSessionManager.shutDown();
+        bgpSessionManager = null;
+    }
+
+    /**
+     * Gets BGP RIB-IN routes by waiting until they are received.
+     * <p/>
+     * NOTE: We keep checking once a second the number of received routes,
+     * up to 5 seconds.
+     *
+     * @param bgpSession the BGP session that is expected to receive the
+     * routes
+     * @param expectedRoutes the expected number of routes
+     * @return the BGP RIB-IN routes as received within the expected
+     * time interval
+     */
+    private Collection<BgpRouteEntry> waitForBgpRibIn(BgpSession bgpSession,
+                                                      long expectedRoutes)
+        throws InterruptedException {
+        Collection<BgpRouteEntry> bgpRibIn = bgpSession.getBgpRibIn();
+
+        final int maxChecks = 5;                // Max wait of 5 seconds
+        for (int i = 0; i < maxChecks; i++) {
+            if (bgpRibIn.size() == expectedRoutes) {
+                break;
+            }
+            Thread.sleep(1000);
+            bgpRibIn = bgpSession.getBgpRibIn();
+        }
+
+        return bgpRibIn;
+    }
+
+    /**
+     * Gets BGP merged routes by waiting until they are received.
+     * <p/>
+     * NOTE: We keep checking once a second the number of received routes,
+     * up to 5 seconds.
+     *
+     * @param expectedRoutes the expected number of routes
+     * @return the BGP Session Manager routes as received within the expected
+     * time interval
+     */
+    private Collection<BgpRouteEntry> waitForBgpRoutes(long expectedRoutes)
+        throws InterruptedException {
+        Collection<BgpRouteEntry> bgpRoutes = bgpSessionManager.getBgpRoutes();
+
+        final int maxChecks = 5;                // Max wait of 5 seconds
+        for (int i = 0; i < maxChecks; i++) {
+            if (bgpRoutes.size() == expectedRoutes) {
+                break;
+            }
+            Thread.sleep(1000);
+            bgpRoutes = bgpSessionManager.getBgpRoutes();
+        }
+
+        return bgpRoutes;
+    }
+
+    /**
+     * Tests that the BGP OPEN messages have been exchanged, followed by
+     * KEEPALIVE.
+     * <p>
+     * The BGP Peer opens the sessions and transmits OPEN Message, eventually
+     * followed by KEEPALIVE. The tested BGP listener should respond by
+     * OPEN Message, followed by KEEPALIVE.
+     *
+     * @throws TestUtilsException TestUtils error
+     */
+    @Test
+    public void testExchangedBgpOpenMessages()
+            throws InterruptedException, TestUtilsException {
+        // Initiate the connection
+        peerBootstrap.connect(connectToSocket);
+
+        // Wait until the OPEN message is received
+        peerFrameDecoder.receivedOpenMessageLatch.await(2000,
+                                                        TimeUnit.MILLISECONDS);
+        // Wait until the KEEPALIVE message is received
+        peerFrameDecoder.receivedKeepaliveMessageLatch.await(2000,
+                                                        TimeUnit.MILLISECONDS);
+
+        //
+        // Test the fields from the BGP OPEN message:
+        // BGP version, AS number, BGP ID
+        //
+        assertThat(peerFrameDecoder.remoteBgpVersion,
+                   is(BgpConstants.BGP_VERSION));
+        assertThat(peerFrameDecoder.remoteAs,
+                   is(TestBgpPeerChannelHandler.PEER_AS));
+        assertThat(peerFrameDecoder.remoteBgpIdentifier, is(IP_LOOPBACK_ID));
+
+        //
+        // Test that a BgpSession instance has been created
+        //
+        assertThat(bgpSessionManager.getMyBgpId(), is(IP_LOOPBACK_ID));
+        assertThat(bgpSessionManager.getBgpSessions(), hasSize(1));
+        BgpSession bgpSession =
+            bgpSessionManager.getBgpSessions().iterator().next();
+        assertThat(bgpSession, notNullValue());
+        long sessionAs = TestUtils.getField(bgpSession, "localAs");
+        assertThat(sessionAs, is(TestBgpPeerChannelHandler.PEER_AS));
+    }
+
+    /**
+     * Tests that the BGP UPDATE messages have been received and processed.
+     */
+    @Test
+    public void testProcessedBgpUpdateMessages() throws InterruptedException {
+        BgpSession bgpSession;
+        IpAddress nextHopRouter;
+        BgpRouteEntry bgpRouteEntry;
+        ChannelBuffer message;
+        Collection<BgpRouteEntry> bgpRibIn;
+        Collection<BgpRouteEntry> bgpRoutes;
+
+        // Initiate the connection
+        peerBootstrap.connect(connectToSocket);
+
+        // Wait until the OPEN message is received
+        peerFrameDecoder.receivedOpenMessageLatch.await(2000,
+                                                        TimeUnit.MILLISECONDS);
+        // Wait until the KEEPALIVE message is received
+        peerFrameDecoder.receivedKeepaliveMessageLatch.await(2000,
+                                                        TimeUnit.MILLISECONDS);
+
+        // Get the BGP Session handler
+        bgpSession = bgpSessionManager.getBgpSessions().iterator().next();
+
+        // Prepare routes to add/delete
+        nextHopRouter = IpAddress.valueOf("10.20.30.40");
+        Collection<IpPrefix> addedRoutes = new LinkedList<>();
+        Collection<IpPrefix> withdrawnRoutes = new LinkedList<>();
+        addedRoutes.add(IpPrefix.valueOf("0.0.0.0/0"));
+        addedRoutes.add(IpPrefix.valueOf("20.0.0.0/8"));
+        addedRoutes.add(IpPrefix.valueOf("30.0.0.0/16"));
+        addedRoutes.add(IpPrefix.valueOf("40.0.0.0/24"));
+        addedRoutes.add(IpPrefix.valueOf("50.0.0.0/32"));
+        withdrawnRoutes.add(IpPrefix.valueOf("60.0.0.0/8"));
+        withdrawnRoutes.add(IpPrefix.valueOf("70.0.0.0/16"));
+        withdrawnRoutes.add(IpPrefix.valueOf("80.0.0.0/24"));
+        withdrawnRoutes.add(IpPrefix.valueOf("90.0.0.0/32"));
+        // Write the routes
+        message = peerChannelHandler.prepareBgpUpdate(nextHopRouter,
+                                                      addedRoutes,
+                                                      withdrawnRoutes);
+        peerChannelHandler.savedCtx.getChannel().write(message);
+
+        // Check that the routes have been received, processed and stored
+        bgpRibIn = waitForBgpRibIn(bgpSession, 5);
+        assertThat(bgpRibIn, hasSize(5));
+        bgpRoutes = waitForBgpRoutes(5);
+        assertThat(bgpRoutes, hasSize(5));
+
+        // Setup the AS Path
+        ArrayList<BgpRouteEntry.PathSegment> pathSegments = new ArrayList<>();
+        byte pathSegmentType1 = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers1 = new ArrayList<>();
+        segmentAsNumbers1.add((long) 65010);
+        segmentAsNumbers1.add((long) 65020);
+        segmentAsNumbers1.add((long) 65030);
+        BgpRouteEntry.PathSegment pathSegment1 =
+            new BgpRouteEntry.PathSegment(pathSegmentType1, segmentAsNumbers1);
+        pathSegments.add(pathSegment1);
+        //
+        byte pathSegmentType2 = (byte) BgpConstants.Update.AsPath.AS_SET;
+        ArrayList<Long> segmentAsNumbers2 = new ArrayList<>();
+        segmentAsNumbers2.add((long) 65041);
+        segmentAsNumbers2.add((long) 65042);
+        segmentAsNumbers2.add((long) 65043);
+        BgpRouteEntry.PathSegment pathSegment2 =
+            new BgpRouteEntry.PathSegment(pathSegmentType2, segmentAsNumbers2);
+        pathSegments.add(pathSegment2);
+        //
+        BgpRouteEntry.AsPath asPath = new BgpRouteEntry.AsPath(pathSegments);
+
+        //
+        bgpRouteEntry =
+            new BgpRouteEntry(bgpSession,
+                              IpPrefix.valueOf("0.0.0.0/0"),
+                              nextHopRouter,
+                              (byte) BgpConstants.Update.Origin.IGP,
+                              asPath,
+                              DEFAULT_LOCAL_PREF);
+        bgpRouteEntry.setMultiExitDisc(DEFAULT_MULTI_EXIT_DISC);
+        assertThat(bgpRibIn, hasItem(bgpRouteEntry));
+        //
+        bgpRouteEntry =
+            new BgpRouteEntry(bgpSession,
+                              IpPrefix.valueOf("20.0.0.0/8"),
+                              nextHopRouter,
+                              (byte) BgpConstants.Update.Origin.IGP,
+                              asPath,
+                              DEFAULT_LOCAL_PREF);
+        bgpRouteEntry.setMultiExitDisc(DEFAULT_MULTI_EXIT_DISC);
+        assertThat(bgpRibIn, hasItem(bgpRouteEntry));
+        //
+        bgpRouteEntry =
+            new BgpRouteEntry(bgpSession,
+                              IpPrefix.valueOf("30.0.0.0/16"),
+                              nextHopRouter,
+                              (byte) BgpConstants.Update.Origin.IGP,
+                              asPath,
+                              DEFAULT_LOCAL_PREF);
+        bgpRouteEntry.setMultiExitDisc(DEFAULT_MULTI_EXIT_DISC);
+        assertThat(bgpRibIn, hasItem(bgpRouteEntry));
+        //
+        bgpRouteEntry =
+            new BgpRouteEntry(bgpSession,
+                              IpPrefix.valueOf("40.0.0.0/24"),
+                              nextHopRouter,
+                              (byte) BgpConstants.Update.Origin.IGP,
+                              asPath,
+                              DEFAULT_LOCAL_PREF);
+        bgpRouteEntry.setMultiExitDisc(DEFAULT_MULTI_EXIT_DISC);
+        assertThat(bgpRibIn, hasItem(bgpRouteEntry));
+        //
+        bgpRouteEntry =
+            new BgpRouteEntry(bgpSession,
+                              IpPrefix.valueOf("50.0.0.0/32"),
+                              nextHopRouter,
+                              (byte) BgpConstants.Update.Origin.IGP,
+                              asPath,
+                              DEFAULT_LOCAL_PREF);
+        bgpRouteEntry.setMultiExitDisc(DEFAULT_MULTI_EXIT_DISC);
+        assertThat(bgpRibIn, hasItem(bgpRouteEntry));
+
+        // Delete some routes
+        addedRoutes = new LinkedList<>();
+        withdrawnRoutes = new LinkedList<>();
+        withdrawnRoutes.add(IpPrefix.valueOf("0.0.0.0/0"));
+        withdrawnRoutes.add(IpPrefix.valueOf("50.0.0.0/32"));
+
+        // Write the routes
+        message = peerChannelHandler.prepareBgpUpdate(nextHopRouter,
+                                                      addedRoutes,
+                                                      withdrawnRoutes);
+        peerChannelHandler.savedCtx.getChannel().write(message);
+
+        // Check that the routes have been received, processed and stored
+        bgpRibIn = waitForBgpRibIn(bgpSession, 3);
+        assertThat(bgpRibIn, hasSize(3));
+        bgpRoutes = waitForBgpRoutes(3);
+        assertThat(bgpRoutes, hasSize(3));
+        //
+        bgpRouteEntry =
+            new BgpRouteEntry(bgpSession,
+                              IpPrefix.valueOf("20.0.0.0/8"),
+                              nextHopRouter,
+                              (byte) BgpConstants.Update.Origin.IGP,
+                              asPath,
+                              DEFAULT_LOCAL_PREF);
+        bgpRouteEntry.setMultiExitDisc(DEFAULT_MULTI_EXIT_DISC);
+        assertThat(bgpRibIn, hasItem(bgpRouteEntry));
+        //
+        bgpRouteEntry =
+            new BgpRouteEntry(bgpSession,
+                              IpPrefix.valueOf("30.0.0.0/16"),
+                              nextHopRouter,
+                              (byte) BgpConstants.Update.Origin.IGP,
+                              asPath,
+                              DEFAULT_LOCAL_PREF);
+        bgpRouteEntry.setMultiExitDisc(DEFAULT_MULTI_EXIT_DISC);
+        assertThat(bgpRibIn, hasItem(bgpRouteEntry));
+        //
+        bgpRouteEntry =
+            new BgpRouteEntry(bgpSession,
+                              IpPrefix.valueOf("40.0.0.0/24"),
+                              nextHopRouter,
+                              (byte) BgpConstants.Update.Origin.IGP,
+                              asPath,
+                              DEFAULT_LOCAL_PREF);
+        bgpRouteEntry.setMultiExitDisc(DEFAULT_MULTI_EXIT_DISC);
+        assertThat(bgpRibIn, hasItem(bgpRouteEntry));
+
+        // Close the channel and test there are no routes
+        peerChannelHandler.closeChannel();
+        bgpRoutes = waitForBgpRoutes(0);
+        assertThat(bgpRoutes, hasSize(0));
+    }
+}
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/PathSegmentTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/PathSegmentTest.java
new file mode 100644
index 0000000..636883e
--- /dev/null
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/PathSegmentTest.java
@@ -0,0 +1,115 @@
+package org.onlab.onos.sdnip.bgp;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+import java.util.ArrayList;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for the BgpRouteEntry.PathSegment class.
+ */
+public class PathSegmentTest {
+    /**
+     * Generates a Path Segment.
+     *
+     * @return a generated PathSegment
+     */
+    private BgpRouteEntry.PathSegment generatePathSegment() {
+        byte pathSegmentType = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers = new ArrayList<>();
+        segmentAsNumbers.add((long) 1);
+        segmentAsNumbers.add((long) 2);
+        segmentAsNumbers.add((long) 3);
+        BgpRouteEntry.PathSegment pathSegment =
+            new BgpRouteEntry.PathSegment(pathSegmentType, segmentAsNumbers);
+
+        return pathSegment;
+    }
+
+    /**
+     * Tests valid class constructor.
+     */
+    @Test
+    public void testConstructor() {
+        BgpRouteEntry.PathSegment pathSegment = generatePathSegment();
+
+        String expectedString =
+            "PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}";
+        assertThat(pathSegment.toString(), is(expectedString));
+    }
+
+    /**
+     * Tests invalid class constructor for null Segment AS Numbers.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullSegmentAsNumbers() {
+        byte pathSegmentType = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers = null;
+        new BgpRouteEntry.PathSegment(pathSegmentType, segmentAsNumbers);
+    }
+
+    /**
+     * Tests getting the fields of a Path Segment.
+     */
+    @Test
+    public void testGetFields() {
+        // Create the fields to compare against
+        byte pathSegmentType = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers = new ArrayList<>();
+        segmentAsNumbers.add((long) 1);
+        segmentAsNumbers.add((long) 2);
+        segmentAsNumbers.add((long) 3);
+
+        // Generate the entry to test
+        BgpRouteEntry.PathSegment pathSegment = generatePathSegment();
+
+        assertThat(pathSegment.getType(), is(pathSegmentType));
+        assertThat(pathSegment.getSegmentAsNumbers(), is(segmentAsNumbers));
+    }
+
+    /**
+     * Tests equality of {@link BgpRouteEntry.PathSegment}.
+     */
+    @Test
+    public void testEquality() {
+        BgpRouteEntry.PathSegment pathSegment1 = generatePathSegment();
+        BgpRouteEntry.PathSegment pathSegment2 = generatePathSegment();
+
+        assertThat(pathSegment1, is(pathSegment2));
+    }
+
+    /**
+     * Tests non-equality of {@link BgpRouteEntry.PathSegment}.
+     */
+    @Test
+    public void testNonEquality() {
+        BgpRouteEntry.PathSegment pathSegment1 = generatePathSegment();
+
+        // Setup Path Segment 2
+        byte pathSegmentType = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        ArrayList<Long> segmentAsNumbers = new ArrayList<>();
+        segmentAsNumbers.add((long) 1);
+        segmentAsNumbers.add((long) 22);                        // Different
+        segmentAsNumbers.add((long) 3);
+        //
+        BgpRouteEntry.PathSegment pathSegment2 =
+            new BgpRouteEntry.PathSegment(pathSegmentType, segmentAsNumbers);
+
+        assertThat(pathSegment1, is(not(pathSegment2)));
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        BgpRouteEntry.PathSegment pathSegment = generatePathSegment();
+
+        String expectedString =
+            "PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}";
+        assertThat(pathSegment.toString(), is(expectedString));
+    }
+}
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/TestBgpPeerChannelHandler.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/TestBgpPeerChannelHandler.java
new file mode 100644
index 0000000..1b51d52
--- /dev/null
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/TestBgpPeerChannelHandler.java
@@ -0,0 +1,250 @@
+package org.onlab.onos.sdnip.bgp;
+
+import java.util.Collection;
+
+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.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+
+/**
+ * Class for handling the remote BGP Peer session.
+ */
+class TestBgpPeerChannelHandler extends SimpleChannelHandler {
+    static final long PEER_AS = 65001;
+    static final int PEER_HOLDTIME = 120;       // 120 seconds
+    final IpAddress bgpId;                     // The BGP ID
+    final long localPref;                       // Local preference for routes
+    final long multiExitDisc = 20;              // MED value
+
+    ChannelHandlerContext savedCtx;
+
+    /**
+     * Constructor for given BGP ID.
+     *
+     * @param bgpId the BGP ID to use
+     * @param localPref the local preference for the routes to use
+     */
+    TestBgpPeerChannelHandler(IpAddress bgpId,
+                              long localPref) {
+        this.bgpId = bgpId;
+        this.localPref = localPref;
+    }
+
+    /**
+     * Closes the channel.
+     */
+    void closeChannel() {
+        savedCtx.getChannel().close();
+    }
+
+    @Override
+    public void channelConnected(ChannelHandlerContext ctx,
+                                 ChannelStateEvent channelEvent) {
+        this.savedCtx = ctx;
+        // Prepare and transmit BGP OPEN message
+        ChannelBuffer message = prepareBgpOpen();
+        ctx.getChannel().write(message);
+
+        // Prepare and transmit BGP KEEPALIVE message
+        message = prepareBgpKeepalive();
+        ctx.getChannel().write(message);
+    }
+
+    @Override
+    public void channelDisconnected(ChannelHandlerContext ctx,
+                                    ChannelStateEvent channelEvent) {
+        // Nothing to do
+    }
+
+    /**
+     * Prepares BGP OPEN message.
+     *
+     * @return the message to transmit (BGP header included)
+     */
+    ChannelBuffer prepareBgpOpen() {
+        ChannelBuffer message =
+            ChannelBuffers.buffer(BgpConstants.BGP_MESSAGE_MAX_LENGTH);
+        message.writeByte(BgpConstants.BGP_VERSION);
+        message.writeShort((int) PEER_AS);
+        message.writeShort(PEER_HOLDTIME);
+        message.writeInt(bgpId.toInt());
+        message.writeByte(0);                   // No Optional Parameters
+        return prepareBgpMessage(BgpConstants.BGP_TYPE_OPEN, message);
+    }
+
+    /**
+     * Prepares BGP UPDATE message.
+     *
+     * @param nextHopRouter the next-hop router address for the routes to add
+     * @param addedRoutes the routes to add
+     * @param withdrawnRoutes the routes to withdraw
+     * @return the message to transmit (BGP header included)
+     */
+    ChannelBuffer prepareBgpUpdate(IpAddress nextHopRouter,
+                                   Collection<IpPrefix> addedRoutes,
+                                   Collection<IpPrefix> withdrawnRoutes) {
+        int attrFlags;
+        ChannelBuffer message =
+            ChannelBuffers.buffer(BgpConstants.BGP_MESSAGE_MAX_LENGTH);
+        ChannelBuffer pathAttributes =
+            ChannelBuffers.buffer(BgpConstants.BGP_MESSAGE_MAX_LENGTH);
+
+        // Encode the Withdrawn Routes
+        ChannelBuffer encodedPrefixes = encodePackedPrefixes(withdrawnRoutes);
+        message.writeShort(encodedPrefixes.readableBytes());
+        message.writeBytes(encodedPrefixes);
+
+        // Encode the Path Attributes
+        // ORIGIN: IGP
+        attrFlags = 0x40;                               // Transitive flag
+        pathAttributes.writeByte(attrFlags);
+        pathAttributes.writeByte(BgpConstants.Update.Origin.TYPE);
+        pathAttributes.writeByte(1);                    // Data length
+        pathAttributes.writeByte(BgpConstants.Update.Origin.IGP);
+        // AS_PATH: Two Path Segments of 3 ASes each
+        attrFlags = 0x40;                               // Transitive flag
+        pathAttributes.writeByte(attrFlags);
+        pathAttributes.writeByte(BgpConstants.Update.AsPath.TYPE);
+        pathAttributes.writeByte(16);                   // Data length
+        byte pathSegmentType1 = (byte) BgpConstants.Update.AsPath.AS_SEQUENCE;
+        pathAttributes.writeByte(pathSegmentType1);
+        pathAttributes.writeByte(3);                    // Three ASes
+        pathAttributes.writeShort(65010);               // AS=65010
+        pathAttributes.writeShort(65020);               // AS=65020
+        pathAttributes.writeShort(65030);               // AS=65030
+        byte pathSegmentType2 = (byte) BgpConstants.Update.AsPath.AS_SET;
+        pathAttributes.writeByte(pathSegmentType2);
+        pathAttributes.writeByte(3);                    // Three ASes
+        pathAttributes.writeShort(65041);               // AS=65041
+        pathAttributes.writeShort(65042);               // AS=65042
+        pathAttributes.writeShort(65043);               // AS=65043
+        // NEXT_HOP: nextHopRouter
+        attrFlags = 0x40;                               // Transitive flag
+        pathAttributes.writeByte(attrFlags);
+        pathAttributes.writeByte(BgpConstants.Update.NextHop.TYPE);
+        pathAttributes.writeByte(4);                    // Data length
+        pathAttributes.writeInt(nextHopRouter.toInt()); // Next-hop router
+        // LOCAL_PREF: localPref
+        attrFlags = 0x40;                               // Transitive flag
+        pathAttributes.writeByte(attrFlags);
+        pathAttributes.writeByte(BgpConstants.Update.LocalPref.TYPE);
+        pathAttributes.writeByte(4);                    // Data length
+        pathAttributes.writeInt((int) localPref);       // Preference value
+        // MULTI_EXIT_DISC: multiExitDisc
+        attrFlags = 0x80;                               // Optional
+                                                        // Non-Transitive flag
+        pathAttributes.writeByte(attrFlags);
+        pathAttributes.writeByte(BgpConstants.Update.MultiExitDisc.TYPE);
+        pathAttributes.writeByte(4);                    // Data length
+        pathAttributes.writeInt((int) multiExitDisc);   // Preference value
+        // The NLRI prefixes
+        encodedPrefixes = encodePackedPrefixes(addedRoutes);
+
+        // Write the Path Attributes, beginning with its length
+        message.writeShort(pathAttributes.readableBytes());
+        message.writeBytes(pathAttributes);
+        message.writeBytes(encodedPrefixes);
+
+        return prepareBgpMessage(BgpConstants.BGP_TYPE_UPDATE, message);
+    }
+
+    /**
+     * Encodes a collection of IPv4 network prefixes in a packed format.
+     * <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 prefixes the prefixes to encode
+     * @return the buffer with the encoded prefixes
+     */
+    private ChannelBuffer encodePackedPrefixes(Collection<IpPrefix> prefixes) {
+        ChannelBuffer message =
+            ChannelBuffers.buffer(BgpConstants.BGP_MESSAGE_MAX_LENGTH);
+
+        // Write each of the prefixes
+        for (IpPrefix prefix : prefixes) {
+            int prefixBitlen = prefix.prefixLength();
+            int prefixBytelen = (prefixBitlen + 7) / 8;         // Round-up
+            message.writeByte(prefixBitlen);
+
+            IpAddress address = prefix.toIpAddress();
+            long value = address.toInt() & 0xffffffffL;
+            for (int i = 0; i < IpAddress.INET_LEN; i++) {
+                if (prefixBytelen-- == 0) {
+                    break;
+                }
+                long nextByte =
+                    (value >> ((IpAddress.INET_LEN - i - 1) * 8)) & 0xff;
+                message.writeByte((int) nextByte);
+            }
+        }
+
+        return message;
+    }
+
+    /**
+     * Prepares BGP KEEPALIVE message.
+     *
+     * @return the message to transmit (BGP header included)
+     */
+    ChannelBuffer prepareBgpKeepalive() {
+        ChannelBuffer message =
+            ChannelBuffers.buffer(BgpConstants.BGP_MESSAGE_MAX_LENGTH);
+        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 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;
+    }
+}
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/TestBgpPeerFrameDecoder.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/TestBgpPeerFrameDecoder.java
new file mode 100644
index 0000000..42bbb90
--- /dev/null
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/TestBgpPeerFrameDecoder.java
@@ -0,0 +1,163 @@
+package org.onlab.onos.sdnip.bgp;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.handler.codec.frame.FrameDecoder;
+import org.onlab.packet.IpAddress;
+
+/**
+ * Class for handling the decoding of the BGP messages at the remote
+ * BGP peer session.
+ */
+class TestBgpPeerFrameDecoder extends FrameDecoder {
+    int remoteBgpVersion;               // 1 octet
+    long remoteAs;                      // 2 octets
+    long remoteHoldtime;                // 2 octets
+    IpAddress remoteBgpIdentifier;     // 4 octets -> IPv4 address
+
+    final CountDownLatch receivedOpenMessageLatch = new CountDownLatch(1);
+    final CountDownLatch receivedKeepaliveMessageLatch = new CountDownLatch(1);
+
+    @Override
+    protected Object decode(ChannelHandlerContext ctx,
+                            Channel channel,
+                            ChannelBuffer buf) throws Exception {
+        // 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
+        //
+        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) {
+                // ERROR: Connection Not Synchronized. Close the channel.
+                ctx.getChannel().close();
+                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)) {
+            // ERROR: Bad Message Length. Close the channel.
+            ctx.getChannel().close();
+            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:
+            processBgpOpen(ctx, message);
+            break;
+        case BgpConstants.BGP_TYPE_UPDATE:
+            // NOTE: Not used as part of the test, because ONOS does not
+            // originate UPDATE messages.
+            break;
+        case BgpConstants.BGP_TYPE_NOTIFICATION:
+            // NOTE: Not used as part of the testing (yet)
+            break;
+        case BgpConstants.BGP_TYPE_KEEPALIVE:
+            processBgpKeepalive(ctx, message);
+            break;
+        default:
+            // ERROR: Bad Message Type. Close the channel.
+            ctx.getChannel().close();
+            return null;
+        }
+
+        return null;
+    }
+
+    /**
+     * Processes BGP OPEN message.
+     *
+     * @param ctx the Channel Handler Context.
+     * @param message the message to process.
+     */
+    private void processBgpOpen(ChannelHandlerContext ctx,
+                                ChannelBuffer message) {
+        int minLength =
+            BgpConstants.BGP_OPEN_MIN_LENGTH - BgpConstants.BGP_HEADER_LENGTH;
+        if (message.readableBytes() < minLength) {
+            // ERROR: Bad Message Length. Close the channel.
+            ctx.getChannel().close();
+            return;
+        }
+
+        //
+        // Parse the OPEN message
+        //
+        remoteBgpVersion = message.readUnsignedByte();
+        remoteAs = message.readUnsignedShort();
+        remoteHoldtime = message.readUnsignedShort();
+        remoteBgpIdentifier = IpAddress.valueOf((int) message.readUnsignedInt());
+        // Optional Parameters
+        int optParamLen = message.readUnsignedByte();
+        if (message.readableBytes() < optParamLen) {
+            // ERROR: Bad Message Length. Close the channel.
+            ctx.getChannel().close();
+            return;
+        }
+        message.readBytes(optParamLen);             // NOTE: data ignored
+
+        // BGP OPEN message successfully received
+        receivedOpenMessageLatch.countDown();
+    }
+
+    /**
+     * Processes BGP KEEPALIVE message.
+     *
+     * @param ctx the Channel Handler Context.
+     * @param message the message to process.
+     */
+    private void processBgpKeepalive(ChannelHandlerContext ctx,
+                                     ChannelBuffer message) {
+        if (message.readableBytes() + BgpConstants.BGP_HEADER_LENGTH !=
+            BgpConstants.BGP_KEEPALIVE_EXPECTED_LENGTH) {
+            // ERROR: Bad Message Length. Close the channel.
+            ctx.getChannel().close();
+            return;
+        }
+        // BGP KEEPALIVE message successfully received
+        receivedKeepaliveMessageLatch.countDown();
+    }
+}
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..39a8bc2 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())
+                .set("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/main/java/org/onlab/onos/net/intent/HostToHostIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
index f420fc2..7a894be6 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
@@ -12,7 +12,7 @@
 /**
  * Abstraction of end-station to end-station bidirectional connectivity.
  */
-public class HostToHostIntent extends ConnectivityIntent {
+public final class HostToHostIntent extends ConnectivityIntent {
 
     private final HostId one;
     private final HostId two;
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/LinkCollectionIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/LinkCollectionIntent.java
index 78c95cf..9d0fde8 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/LinkCollectionIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/LinkCollectionIntent.java
@@ -14,7 +14,7 @@
  * Abstraction of a connectivity intent that is implemented by a set of path
  * segments.
  */
-public class LinkCollectionIntent extends ConnectivityIntent implements InstallableIntent {
+public final class LinkCollectionIntent extends ConnectivityIntent implements InstallableIntent {
 
     private final Set<Link> links;
 
@@ -46,6 +46,12 @@
         return links;
     }
 
+    /**
+     * Returns the set of links that represent the network connections needed
+     * by this intent.
+     *
+     * @return Set of links for the network hops needed by this intent
+     */
     public Set<Link> links() {
         return links;
     }
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java
index be8d309..8ee4a9e 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java
@@ -15,7 +15,7 @@
 /**
  * Abstraction of multiple source to single destination connectivity intent.
  */
-public class MultiPointToSinglePointIntent extends ConnectivityIntent {
+public final class MultiPointToSinglePointIntent extends ConnectivityIntent {
 
     private final Set<ConnectPoint> ingressPoints;
     private final ConnectPoint egressPoint;
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/NetTestTools.java b/core/api/src/test/java/org/onlab/onos/net/NetTestTools.java
index e1e78ed..6fc38e3 100644
--- a/core/api/src/test/java/org/onlab/onos/net/NetTestTools.java
+++ b/core/api/src/test/java/org/onlab/onos/net/NetTestTools.java
@@ -48,10 +48,16 @@
                                new HashSet<IpPrefix>());
     }
 
+    // Short-hand for creating a connection point.
+    public static ConnectPoint connectPoint(String id, int port) {
+        return new ConnectPoint(did(id), portNumber(port));
+    }
+
     // Short-hand for creating a link.
     public static Link link(String src, int sp, String dst, int dp) {
-        return new DefaultLink(PID, new ConnectPoint(did(src), portNumber(sp)),
-                               new ConnectPoint(did(dst), portNumber(dp)),
+        return new DefaultLink(PID,
+                               connectPoint(src, sp),
+                               connectPoint(dst, dp),
                                Link.Type.DIRECT);
     }
 
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/store/hz/net/pom.xml b/core/json/pom.xml
similarity index 74%
rename from core/store/hz/net/pom.xml
rename to core/json/pom.xml
index 177e99e..63dfe57 100644
--- a/core/store/hz/net/pom.xml
+++ b/core/json/pom.xml
@@ -6,15 +6,15 @@
 
     <parent>
         <groupId>org.onlab.onos</groupId>
-        <artifactId>onos-core-hz</artifactId>
+        <artifactId>onos-core</artifactId>
         <version>1.0.0-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
 
-    <artifactId>onos-core-hz-net</artifactId>
+    <artifactId>onos-json</artifactId>
     <packaging>bundle</packaging>
 
-    <description>ONOS Hazelcast based distributed store subsystems</description>
+    <description>ONOS JSON encode/decode facilities</description>
 
     <dependencies>
         <dependency>
@@ -23,24 +23,22 @@
         </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>
+            <artifactId>onos-api</artifactId>
             <classifier>tests</classifier>
             <scope>test</scope>
-            <version>${project.version}</version>
         </dependency>
+
+        <dependency>
+            <groupId>org.onlab.onos</groupId>
+            <artifactId>onos-core-trivial</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </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>
diff --git a/core/json/src/main/java/org/onlab/onos/json/impl/DeleteMe.java b/core/json/src/main/java/org/onlab/onos/json/impl/DeleteMe.java
new file mode 100644
index 0000000..d89f31b
--- /dev/null
+++ b/core/json/src/main/java/org/onlab/onos/json/impl/DeleteMe.java
@@ -0,0 +1,7 @@
+package org.onlab.onos.json.impl;
+
+/**
+ * Created by tom on 10/16/14.
+ */
+public class DeleteMe {
+}
diff --git a/core/json/src/main/java/org/onlab/onos/json/impl/package-info.java b/core/json/src/main/java/org/onlab/onos/json/impl/package-info.java
new file mode 100644
index 0000000..406641f
--- /dev/null
+++ b/core/json/src/main/java/org/onlab/onos/json/impl/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Implementation of JSON codec factory and of the builtin codecs.
+ */
+package org.onlab.onos.json.impl;
\ No newline at end of file
diff --git a/core/net/pom.xml b/core/net/pom.xml
index 6518068..bff4cce 100644
--- a/core/net/pom.xml
+++ b/core/net/pom.xml
@@ -36,26 +36,9 @@
             <scope>test</scope>
         </dependency>
 
-	<dependency>
-	    <groupId>org.easymock</groupId>
-	    <artifactId>easymock</artifactId>
-	    <scope>test</scope>
-	</dependency>
-
-        <!-- TODO Consider removing store dependency.
-              Currently required for DistributedDeviceManagerTest. -->
         <dependency>
-            <groupId>org.onlab.onos</groupId>
-            <artifactId>onos-core-hz-net</artifactId>
-            <version>${project.version}</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.onlab.onos</groupId>
-            <!-- FIXME: should be somewhere else -->
-            <artifactId>onos-core-hz-common</artifactId>
-            <version>${project.version}</version>
-            <classifier>tests</classifier>
+            <groupId>org.easymock</groupId>
+            <artifactId>easymock</artifactId>
             <scope>test</scope>
         </dependency>
 
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/intent/impl/HostToHostIntentCompiler.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/HostToHostIntentCompiler.java
index de61e8e..50faf38 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/HostToHostIntentCompiler.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/HostToHostIntentCompiler.java
@@ -41,7 +41,7 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected HostService hostService;
 
-    private IdGenerator<IntentId> intentIdGenerator;
+    protected IdGenerator<IntentId> intentIdGenerator;
 
     @Activate
     public void activate() {
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/MultiPointToSinglePointIntentCompiler.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/MultiPointToSinglePointIntentCompiler.java
index 68c55dd..aecd3ba 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/MultiPointToSinglePointIntentCompiler.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/MultiPointToSinglePointIntentCompiler.java
@@ -37,7 +37,7 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected PathService pathService;
 
-    private IdGenerator<IntentId> intentIdGenerator;
+    protected IdGenerator<IntentId> intentIdGenerator;
 
     @Activate
     public void activate() {
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/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java b/core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java
new file mode 100644
index 0000000..0f020a4
--- /dev/null
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java
@@ -0,0 +1,86 @@
+package org.onlab.onos.net.intent;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.onlab.onos.net.ElementId;
+import org.onlab.onos.net.Path;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.flow.criteria.Criterion;
+import org.onlab.onos.net.flow.instructions.Instruction;
+import org.onlab.onos.net.topology.LinkWeight;
+import org.onlab.onos.net.topology.PathService;
+
+import static org.onlab.onos.net.NetTestTools.createPath;
+
+/**
+ * Common mocks used by the intent framework tests.
+ */
+public class IntentTestsMocks {
+    /**
+     * Mock traffic selector class used for satisfying API requirements.
+     */
+    public static class MockSelector implements TrafficSelector {
+        @Override
+        public Set<Criterion> criteria() {
+            return new HashSet<>();
+        }
+    }
+
+    /**
+     * Mock traffic treatment class used for satisfying API requirements.
+     */
+    public static class MockTreatment implements TrafficTreatment {
+        @Override
+        public List<Instruction> instructions() {
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * Mock path service for creating paths within the test.
+     */
+    public static class MockPathService implements PathService {
+
+        final String[] pathHops;
+        final String[] reversePathHops;
+
+        /**
+         * Constructor that provides a set of hops to mock.
+         *
+         * @param pathHops path hops to mock
+         */
+        public MockPathService(String[] pathHops) {
+            this.pathHops = pathHops;
+            String[] reversed = pathHops.clone();
+            Collections.reverse(Arrays.asList(reversed));
+            reversePathHops = reversed;
+        }
+
+        @Override
+        public Set<Path> getPaths(ElementId src, ElementId dst) {
+            Set<Path> result = new HashSet<>();
+
+            String[] allHops = new String[pathHops.length];
+
+            if (src.toString().endsWith(pathHops[0])) {
+                System.arraycopy(pathHops, 0, allHops, 0, pathHops.length);
+            } else {
+                System.arraycopy(reversePathHops, 0, allHops, 0, pathHops.length);
+            }
+
+            result.add(createPath(allHops));
+            return result;
+        }
+
+        @Override
+        public Set<Path> getPaths(ElementId src, ElementId dst, LinkWeight weight) {
+            return getPaths(src, dst);
+        }
+    }
+}
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/LinksHaveEntryWithSourceDestinationPairMatcher.java b/core/net/src/test/java/org/onlab/onos/net/intent/LinksHaveEntryWithSourceDestinationPairMatcher.java
new file mode 100644
index 0000000..869cefe
--- /dev/null
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/LinksHaveEntryWithSourceDestinationPairMatcher.java
@@ -0,0 +1,73 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Collection;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.onlab.onos.net.Link;
+
+/**
+ * Matcher to determine if a Collection of Links contains a path between a source
+ * and a destination.
+ */
+public class LinksHaveEntryWithSourceDestinationPairMatcher extends
+        TypeSafeMatcher<Collection<Link>> {
+    private final String source;
+    private final String destination;
+
+    /**
+     * Creates a matcher for a given path represented by a source and
+     * a destination.
+     *
+     * @param source string identifier for the source of the path
+     * @param destination string identifier for the destination of the path
+     */
+    LinksHaveEntryWithSourceDestinationPairMatcher(String source,
+                                                   String destination) {
+        this.source = source;
+        this.destination = destination;
+    }
+
+    @Override
+    public boolean matchesSafely(Collection<Link> links) {
+        for (Link link : links) {
+            if (link.src().elementId().toString().endsWith(source) &&
+                    link.dst().elementId().toString().endsWith(destination)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("link lookup for source \"");
+        description.appendText(source);
+        description.appendText(" and destination ");
+        description.appendText(destination);
+        description.appendText("\"");
+    }
+
+    @Override
+    public void describeMismatchSafely(Collection<Link> links,
+                                       Description mismatchDescription) {
+        mismatchDescription.appendText("was ").
+                appendText(links.toString());
+    }
+
+    /**
+     * Creates a link has path matcher.
+     *
+     * @param source string identifier for the source of the path
+     * @param destination string identifier for the destination of the path
+     * @return matcher to match the path
+     */
+    public static LinksHaveEntryWithSourceDestinationPairMatcher linksHasPath(
+            String source,
+            String destination) {
+        return new LinksHaveEntryWithSourceDestinationPairMatcher(source,
+                destination);
+    }
+}
+
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/TestHostToHostIntent.java b/core/net/src/test/java/org/onlab/onos/net/intent/TestHostToHostIntent.java
new file mode 100644
index 0000000..f5e2551
--- /dev/null
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/TestHostToHostIntent.java
@@ -0,0 +1,112 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.Test;
+import org.onlab.onos.net.HostId;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.onlab.onos.net.NetTestTools.hid;
+
+/**
+ * Unit tests for the HostToHostIntent class.
+ */
+public class TestHostToHostIntent {
+
+    private TrafficSelector selector = new IntentTestsMocks.MockSelector();
+    private TrafficTreatment treatment = new IntentTestsMocks.MockTreatment();
+
+    private HostToHostIntent makeHostToHost(long id, HostId one, HostId two) {
+        return new HostToHostIntent(new IntentId(id),
+                                    one,
+                                    two,
+                                    selector,
+                                    treatment);
+    }
+
+    /**
+     * Tests the equals() method where two HostToHostIntents have references
+     * to the same hosts. These should compare equal.
+     */
+    @Test
+    public void testSameEquals() {
+
+        HostId one = hid("00:00:00:00:00:01/-1");
+        HostId two = hid("00:00:00:00:00:02/-1");
+        HostToHostIntent i1 = makeHostToHost(12, one, two);
+        HostToHostIntent i2 = makeHostToHost(12, one, two);
+
+        assertThat(i1, is(equalTo(i2)));
+    }
+
+    /**
+     * Tests the equals() method where two HostToHostIntents have references
+     * to different Hosts. These should compare not equal.
+     */
+    @Test
+    public void testLinksDifferentEquals() {
+
+        HostId one = hid("00:00:00:00:00:01/-1");
+        HostId two = hid("00:00:00:00:00:02/-1");
+        HostToHostIntent i1 = makeHostToHost(12, one, two);
+        HostToHostIntent i2 = makeHostToHost(12, two, one);
+
+        assertThat(i1, is(not(equalTo(i2))));
+    }
+
+    /**
+     * Tests the equals() method where two HostToHostIntents have different
+     * ids. These should compare not equal.
+     */
+
+    @Test
+    public void testBaseDifferentEquals() {
+        HostId one = hid("00:00:00:00:00:01/-1");
+        HostId two = hid("00:00:00:00:00:02/-1");
+        HostToHostIntent i1 = makeHostToHost(12, one, two);
+        HostToHostIntent i2 = makeHostToHost(11, one, two);
+
+        assertThat(i1, is(not(equalTo(i2))));
+    }
+
+    /**
+     * Tests that the hashCode() values for two equivalent HostToHostIntent
+     * objects are the same.
+     */
+
+    @Test
+    public void testHashCodeEquals() {
+        HostId one = hid("00:00:00:00:00:01/-1");
+        HostId two = hid("00:00:00:00:00:02/-1");
+        HostToHostIntent i1 = makeHostToHost(12, one, two);
+        HostToHostIntent i2 = makeHostToHost(12, one, two);
+
+        assertThat(i1.hashCode(), is(equalTo(i2.hashCode())));
+    }
+
+    /**
+     * Tests that the hashCode() values for two distinct LinkCollectionIntent
+     * objects are different.
+     */
+
+    @Test
+    public void testHashCodeDifferent() {
+        HostId one = hid("00:00:00:00:00:01/-1");
+        HostId two = hid("00:00:00:00:00:02/-1");
+        HostToHostIntent i1 = makeHostToHost(12, one, two);
+        HostToHostIntent i2 = makeHostToHost(112, one, two);
+
+        assertThat(i1.hashCode(), is(not(equalTo(i2.hashCode()))));
+    }
+
+    /**
+     * Checks that the HostToHostIntent class is immutable.
+     */
+    @Test
+    public void checkImmutability() {
+        ImmutableClassChecker.assertThatClassIsImmutable(HostToHostIntent.class);
+    }
+}
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java b/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java
index ba67a6a..f781cf3 100644
--- a/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java
@@ -1,47 +1,145 @@
 package org.onlab.onos.net.intent;
 
-import java.util.ArrayList;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
-import org.onlab.onos.net.flow.criteria.Criterion;
-import org.onlab.onos.net.flow.instructions.Instruction;
 
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
+import static org.onlab.onos.net.NetTestTools.link;
 
+/**
+ * Unit tests for the LinkCollectionIntent class.
+ */
 public class TestLinkCollectionIntent {
 
-    private static class MockSelector implements TrafficSelector {
-        @Override
-        public Set<Criterion> criteria() {
-            return new HashSet<Criterion>();
-        }
+    private Link link1 = link("dev1", 1, "dev2", 2);
+    private Link link2 = link("dev1", 1, "dev3", 2);
+    private Link link3 = link("dev2", 1, "dev3", 2);
+
+    private Set<Link> links1;
+    private Set<Link> links2;
+
+    private TrafficSelector selector = new IntentTestsMocks.MockSelector();
+    private TrafficTreatment treatment = new IntentTestsMocks.MockTreatment();
+
+    private LinkCollectionIntent makeLinkCollection(long id, Set<Link> links) {
+        return new LinkCollectionIntent(new IntentId(id),
+                                        selector, treatment, links);
     }
 
-    private static class MockTreatment implements TrafficTreatment {
-        @Override
-        public List<Instruction> instructions() {
-            return new ArrayList<>();
-        }
+    @Before
+    public void setup() {
+        links1 = new HashSet<>();
+        links2 = new HashSet<>();
     }
 
+    /**
+     * Tests the equals() method where two LinkCollectionIntents have references
+     * to the same Links in different orders. These should compare equal.
+     */
     @Test
-    public void testComparison() {
-        TrafficSelector selector = new MockSelector();
-        TrafficTreatment treatment = new MockTreatment();
-        Set<Link> links = new HashSet<>();
-        LinkCollectionIntent i1 = new LinkCollectionIntent(new IntentId(12),
-                selector, treatment, links);
-        LinkCollectionIntent i2 = new LinkCollectionIntent(new IntentId(12),
-                selector, treatment, links);
+    public void testSameEquals() {
+        links1.add(link1);
+        links1.add(link2);
+        links1.add(link3);
 
-        assertThat(i1.equals(i2), is(true));
+        links2.add(link3);
+        links2.add(link2);
+        links2.add(link1);
+
+        LinkCollectionIntent i1 = makeLinkCollection(12, links1);
+        LinkCollectionIntent i2 = makeLinkCollection(12, links2);
+
+        assertThat(i1, is(equalTo(i2)));
     }
 
+    /**
+     * Tests the equals() method where two LinkCollectionIntents have references
+     * to different Links. These should compare not equal.
+     */
+    @Test
+    public void testLinksDifferentEquals() {
+        links1.add(link1);
+        links1.add(link2);
+
+        links2.add(link3);
+        links2.add(link1);
+
+        LinkCollectionIntent i1 = makeLinkCollection(12, links1);
+        LinkCollectionIntent i2 = makeLinkCollection(12, links2);
+
+        assertThat(i1, is(not(equalTo(i2))));
+    }
+
+    /**
+     * Tests the equals() method where two LinkCollectionIntents have different
+     * ids. These should compare not equal.
+     */
+    @Test
+    public void testBaseDifferentEquals() {
+        links1.add(link1);
+        links1.add(link2);
+
+        links2.add(link2);
+        links2.add(link1);
+
+        LinkCollectionIntent i1 = makeLinkCollection(1, links1);
+        LinkCollectionIntent i2 = makeLinkCollection(2, links2);
+
+        assertThat(i1, is(not(equalTo(i2))));
+    }
+
+    /**
+     * Tests that the hashCode() values for two equivalent LinkCollectionIntent
+     * objects are the same.
+     */
+    @Test
+    public void testHashCodeEquals() {
+        links1.add(link1);
+        links1.add(link2);
+        links1.add(link3);
+
+        links2.add(link3);
+        links2.add(link2);
+        links2.add(link1);
+
+        LinkCollectionIntent i1 = makeLinkCollection(1, links1);
+        LinkCollectionIntent i2 = makeLinkCollection(1, links2);
+
+        assertThat(i1.hashCode(), is(equalTo(i2.hashCode())));
+    }
+
+    /**
+     * Tests that the hashCode() values for two distinct LinkCollectionIntent
+     * objects are different.
+     */
+    @Test
+    public void testHashCodeDifferent() {
+        links1.add(link1);
+        links1.add(link2);
+
+        links2.add(link1);
+        links2.add(link3);
+
+        LinkCollectionIntent i1 = makeLinkCollection(1, links1);
+        LinkCollectionIntent i2 = makeLinkCollection(1, links2);
+
+        assertThat(i1.hashCode(), is(not(equalTo(i2.hashCode()))));
+    }
+
+    /**
+     * Checks that the HostToHostIntent class is immutable.
+     */
+    @Test
+    public void checkImmutability() {
+        ImmutableClassChecker.assertThatClassIsImmutable(LinkCollectionIntent.class);
+    }
 }
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/TestMultiPointToSinglePointIntent.java b/core/net/src/test/java/org/onlab/onos/net/intent/TestMultiPointToSinglePointIntent.java
new file mode 100644
index 0000000..e921907
--- /dev/null
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/TestMultiPointToSinglePointIntent.java
@@ -0,0 +1,157 @@
+package org.onlab.onos.net.intent;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.onlab.onos.net.NetTestTools.connectPoint;
+
+/**
+ * Unit tests for the MultiPointToSinglePointIntent class.
+ */
+public class TestMultiPointToSinglePointIntent {
+
+    private ConnectPoint point1 = connectPoint("dev1", 1);
+    private ConnectPoint point2 = connectPoint("dev2", 1);
+    private ConnectPoint point3 = connectPoint("dev3", 1);
+
+    private TrafficSelector selector = new IntentTestsMocks.MockSelector();
+    private TrafficTreatment treatment = new IntentTestsMocks.MockTreatment();
+
+    Set<ConnectPoint> ingress1;
+    Set<ConnectPoint> ingress2;
+
+    /**
+     * Creates a MultiPointToSinglePointIntent object.
+     *
+     * @param id identifier to use for the new intent
+     * @param ingress set of ingress points
+     * @param egress egress point
+     * @return MultiPointToSinglePoint intent
+     */
+    private MultiPointToSinglePointIntent makeIntent(long id,
+                       Set<ConnectPoint> ingress,
+                       ConnectPoint egress) {
+        return new MultiPointToSinglePointIntent(new IntentId(id),
+                                                 selector,
+                                                 treatment,
+                                                 ingress,
+                                                 egress);
+    }
+
+    /**
+     * Initializes the ingress sets.
+     */
+    @Before
+    public void setup() {
+        ingress1 = new HashSet<>();
+        ingress2 = new HashSet<>();
+    }
+
+    /**
+     * Tests the equals() method where two MultiPointToSinglePoint have references
+     * to the same Links in different orders. These should compare equal.
+     */
+    @Test
+    public void testSameEquals() {
+
+        Set<ConnectPoint> ingress1 = new HashSet<>();
+        ingress1.add(point2);
+        ingress1.add(point3);
+
+        Set<ConnectPoint> ingress2 = new HashSet<>();
+        ingress2.add(point3);
+        ingress2.add(point2);
+
+        Intent i1 = makeIntent(12, ingress1, point1);
+        Intent i2 = makeIntent(12, ingress2, point1);
+
+        assertThat(i1, is(equalTo(i2)));
+    }
+
+    /**
+     * Tests the equals() method where two MultiPointToSinglePoint have references
+     * to different Links. These should compare not equal.
+     */
+    @Test
+    public void testLinksDifferentEquals() {
+        ingress1.add(point3);
+
+        ingress2.add(point3);
+        ingress2.add(point2);
+
+        Intent i1 = makeIntent(12, ingress1, point1);
+        Intent i2 = makeIntent(12, ingress2, point1);
+
+        assertThat(i1, is(not(equalTo(i2))));
+    }
+
+    /**
+     * Tests the equals() method where two MultiPointToSinglePoint have different
+     * ids. These should compare not equal.
+     */
+    @Test
+    public void testBaseDifferentEquals() {
+        ingress1.add(point3);
+        ingress2.add(point3);
+
+        Intent i1 = makeIntent(12, ingress1, point1);
+        Intent i2 = makeIntent(11, ingress2, point1);
+
+        assertThat(i1, is(not(equalTo(i2))));
+    }
+
+    /**
+     * Tests that the hashCode() values for two equivalent MultiPointToSinglePoint
+     * objects are the same.
+     */
+    @Test
+    public void testHashCodeEquals() {
+        ingress1.add(point2);
+        ingress1.add(point3);
+
+        ingress2.add(point3);
+        ingress2.add(point2);
+
+        Intent i1 = makeIntent(12, ingress1, point1);
+        Intent i2 = makeIntent(12, ingress2, point1);
+
+        assertThat(i1.hashCode(), is(equalTo(i2.hashCode())));
+    }
+
+    /**
+     * Tests that the hashCode() values for two distinct MultiPointToSinglePoint
+     * objects are different.
+     */
+    @Test
+    public void testHashCodeDifferent() {
+        ingress1.add(point2);
+
+        ingress2.add(point3);
+        ingress2.add(point2);
+
+        Intent i1 = makeIntent(12, ingress1, point1);
+        Intent i2 = makeIntent(12, ingress2, point1);
+
+
+        assertThat(i1.hashCode(), is(not(equalTo(i2.hashCode()))));
+    }
+
+    /**
+     * Checks that the MultiPointToSinglePointIntent class is immutable.
+     */
+    @Test
+    public void checkImmutability() {
+        ImmutableClassChecker.
+                assertThatClassIsImmutable(MultiPointToSinglePointIntent.class);
+    }
+}
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/impl/TestHostToHostIntentCompiler.java b/core/net/src/test/java/org/onlab/onos/net/intent/impl/TestHostToHostIntentCompiler.java
new file mode 100644
index 0000000..bd61b7a
--- /dev/null
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/impl/TestHostToHostIntentCompiler.java
@@ -0,0 +1,151 @@
+package org.onlab.onos.net.intent.impl;
+
+import java.util.List;
+
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.onos.net.Host;
+import org.onlab.onos.net.HostId;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.host.HostService;
+import org.onlab.onos.net.intent.HostToHostIntent;
+import org.onlab.onos.net.intent.Intent;
+import org.onlab.onos.net.intent.IntentId;
+import org.onlab.onos.net.intent.IntentTestsMocks;
+import org.onlab.onos.net.intent.PathIntent;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.onlab.onos.net.NetTestTools.hid;
+import static org.onlab.onos.net.intent.LinksHaveEntryWithSourceDestinationPairMatcher.linksHasPath;
+
+/**
+ * Unit tests for the HostToHost intent compiler.
+ */
+public class TestHostToHostIntentCompiler {
+    private static final String HOST_ONE_MAC = "00:00:00:00:00:01";
+    private static final String HOST_TWO_MAC = "00:00:00:00:00:02";
+    private static final String HOST_ONE_VLAN = "-1";
+    private static final String HOST_TWO_VLAN = "-1";
+    private static final String HOST_ONE = HOST_ONE_MAC + "/" + HOST_ONE_VLAN;
+    private static final String HOST_TWO = HOST_TWO_MAC + "/" + HOST_TWO_VLAN;
+
+    private TrafficSelector selector = new IntentTestsMocks.MockSelector();
+    private TrafficTreatment treatment = new IntentTestsMocks.MockTreatment();
+
+    private HostId hostOneId = HostId.hostId(HOST_ONE);
+    private HostId hostTwoId = HostId.hostId(HOST_TWO);
+    private HostService mockHostService;
+
+    @Before
+    public void setup() {
+        Host hostOne = createMock(Host.class);
+        expect(hostOne.mac()).andReturn(new MacAddress(HOST_ONE_MAC.getBytes())).anyTimes();
+        expect(hostOne.vlan()).andReturn(VlanId.vlanId()).anyTimes();
+        replay(hostOne);
+
+        Host hostTwo = createMock(Host.class);
+        expect(hostTwo.mac()).andReturn(new MacAddress(HOST_TWO_MAC.getBytes())).anyTimes();
+        expect(hostTwo.vlan()).andReturn(VlanId.vlanId()).anyTimes();
+        replay(hostTwo);
+
+        mockHostService = createMock(HostService.class);
+        expect(mockHostService.getHost(eq(hostOneId))).andReturn(hostOne).anyTimes();
+        expect(mockHostService.getHost(eq(hostTwoId))).andReturn(hostTwo).anyTimes();
+        replay(mockHostService);
+    }
+
+    /**
+     * Creates a HostToHost intent based on two host Ids.
+     *
+     * @param oneIdString string for host one id
+     * @param twoIdString string for host two id
+     * @return HostToHostIntent for the two hosts
+     */
+    private HostToHostIntent makeIntent(String oneIdString, String twoIdString) {
+        return new HostToHostIntent(new IntentId(12),
+                                    hid(oneIdString),
+                                    hid(twoIdString),
+                                    selector,
+                                    treatment);
+    }
+
+    /**
+     * Creates a compiler for HostToHost intents.
+     *
+     * @param hops string array describing the path hops to use when compiling
+     * @return HostToHost intent compiler
+     */
+    private HostToHostIntentCompiler makeCompiler(String[] hops) {
+        HostToHostIntentCompiler compiler =
+                new HostToHostIntentCompiler();
+        compiler.pathService = new IntentTestsMocks.MockPathService(hops);
+        compiler.hostService = mockHostService;
+        IdBlockAllocator idBlockAllocator = new DummyIdBlockAllocator();
+        compiler.intentIdGenerator =
+                new IdBlockAllocatorBasedIntentIdGenerator(idBlockAllocator);
+        return compiler;
+    }
+
+
+    /**
+     * Tests a pair of hosts with 8 hops between them.
+     */
+    @Test
+    public void testSingleLongPathCompilation() {
+
+        HostToHostIntent intent = makeIntent(HOST_ONE,
+                                             HOST_TWO);
+        assertThat(intent, is(notNullValue()));
+
+        String[] hops = {HOST_ONE, "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", HOST_TWO};
+        HostToHostIntentCompiler compiler = makeCompiler(hops);
+        assertThat(compiler, is(notNullValue()));
+
+        List<Intent> result = compiler.compile(intent);
+        assertThat(result, is(Matchers.notNullValue()));
+        assertThat(result, hasSize(2));
+        Intent forwardResultIntent = result.get(0);
+        assertThat(forwardResultIntent instanceof PathIntent, is(true));
+        Intent reverseResultIntent = result.get(1);
+        assertThat(reverseResultIntent instanceof PathIntent, is(true));
+
+        if (forwardResultIntent instanceof PathIntent) {
+            PathIntent forwardPathIntent = (PathIntent) forwardResultIntent;
+            assertThat(forwardPathIntent.path().links(), hasSize(9));
+            assertThat(forwardPathIntent.path().links(), linksHasPath(HOST_ONE, "h1"));
+            assertThat(forwardPathIntent.path().links(), linksHasPath("h1", "h2"));
+            assertThat(forwardPathIntent.path().links(), linksHasPath("h2", "h3"));
+            assertThat(forwardPathIntent.path().links(), linksHasPath("h3", "h4"));
+            assertThat(forwardPathIntent.path().links(), linksHasPath("h4", "h5"));
+            assertThat(forwardPathIntent.path().links(), linksHasPath("h5", "h6"));
+            assertThat(forwardPathIntent.path().links(), linksHasPath("h6", "h7"));
+            assertThat(forwardPathIntent.path().links(), linksHasPath("h7", "h8"));
+            assertThat(forwardPathIntent.path().links(), linksHasPath("h8", HOST_TWO));
+        }
+
+        if (reverseResultIntent instanceof PathIntent) {
+            PathIntent reversePathIntent = (PathIntent) reverseResultIntent;
+            assertThat(reversePathIntent.path().links(), hasSize(9));
+            assertThat(reversePathIntent.path().links(), linksHasPath("h1", HOST_ONE));
+            assertThat(reversePathIntent.path().links(), linksHasPath("h2", "h1"));
+            assertThat(reversePathIntent.path().links(), linksHasPath("h3", "h2"));
+            assertThat(reversePathIntent.path().links(), linksHasPath("h4", "h3"));
+            assertThat(reversePathIntent.path().links(), linksHasPath("h5", "h4"));
+            assertThat(reversePathIntent.path().links(), linksHasPath("h6", "h5"));
+            assertThat(reversePathIntent.path().links(), linksHasPath("h7", "h6"));
+            assertThat(reversePathIntent.path().links(), linksHasPath("h8", "h7"));
+            assertThat(reversePathIntent.path().links(), linksHasPath(HOST_TWO, "h8"));
+        }
+    }
+}
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/impl/TestMultiPointToSinglePointIntentCompiler.java b/core/net/src/test/java/org/onlab/onos/net/intent/impl/TestMultiPointToSinglePointIntentCompiler.java
new file mode 100644
index 0000000..8d286cf
--- /dev/null
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/impl/TestMultiPointToSinglePointIntentCompiler.java
@@ -0,0 +1,214 @@
+package org.onlab.onos.net.intent.impl;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.hamcrest.Matchers;
+import org.junit.Test;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.ElementId;
+import org.onlab.onos.net.Path;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.intent.Intent;
+import org.onlab.onos.net.intent.IntentId;
+import org.onlab.onos.net.intent.IntentTestsMocks;
+import org.onlab.onos.net.intent.LinkCollectionIntent;
+import org.onlab.onos.net.intent.MultiPointToSinglePointIntent;
+import org.onlab.onos.net.topology.LinkWeight;
+import org.onlab.onos.net.topology.PathService;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.onlab.onos.net.NetTestTools.connectPoint;
+import static org.onlab.onos.net.NetTestTools.createPath;
+import static org.onlab.onos.net.intent.LinksHaveEntryWithSourceDestinationPairMatcher.linksHasPath;
+
+/**
+ * Unit tests for the MultiPointToSinglePoint intent compiler.
+ */
+public class TestMultiPointToSinglePointIntentCompiler {
+
+    private TrafficSelector selector = new IntentTestsMocks.MockSelector();
+    private TrafficTreatment treatment = new IntentTestsMocks.MockTreatment();
+
+    /**
+     * Mock path service for creating paths within the test.
+     */
+    private static class MockPathService implements PathService {
+
+        final String[] pathHops;
+
+        /**
+         * Constructor that provides a set of hops to mock.
+         *
+         * @param pathHops path hops to mock
+         */
+        MockPathService(String[] pathHops) {
+            this.pathHops = pathHops;
+        }
+
+        @Override
+        public Set<Path> getPaths(ElementId src, ElementId dst) {
+            Set<Path> result = new HashSet<>();
+
+            String[] allHops = new String[pathHops.length + 1];
+            allHops[0] = src.toString();
+            System.arraycopy(pathHops, 0, allHops, 1, pathHops.length);
+
+            result.add(createPath(allHops));
+            return result;
+        }
+
+        @Override
+        public Set<Path> getPaths(ElementId src, ElementId dst, LinkWeight weight) {
+            return null;
+        }
+    }
+
+    /**
+     * Creates a MultiPointToSinglePoint intent for a group of ingress points
+     * and an egress point.
+     *
+     * @param ingressIds array of ingress device ids
+     * @param egressId device id of the egress point
+     * @return MultiPointToSinglePoint intent
+     */
+    private MultiPointToSinglePointIntent makeIntent(String[] ingressIds, String egressId) {
+        Set<ConnectPoint> ingressPoints = new HashSet<>();
+        ConnectPoint egressPoint = connectPoint(egressId, 1);
+
+        for (String ingressId : ingressIds) {
+                ingressPoints.add(connectPoint(ingressId, 1));
+        }
+
+        return new MultiPointToSinglePointIntent(
+                new IntentId(12),
+                selector,
+                treatment,
+                ingressPoints,
+                egressPoint);
+    }
+
+    /**
+     * Creates a compiler for MultiPointToSinglePoint intents.
+     *
+     * @param hops hops to use while computing paths for this intent
+     * @return MultiPointToSinglePoint intent
+     */
+    private MultiPointToSinglePointIntentCompiler makeCompiler(String[] hops) {
+        MultiPointToSinglePointIntentCompiler compiler =
+                new MultiPointToSinglePointIntentCompiler();
+        compiler.pathService = new MockPathService(hops);
+        IdBlockAllocator idBlockAllocator = new DummyIdBlockAllocator();
+        compiler.intentIdGenerator =
+                new IdBlockAllocatorBasedIntentIdGenerator(idBlockAllocator);
+        return compiler;
+    }
+
+    /**
+     * Tests a single ingress point with 8 hops to its egress point.
+     */
+    @Test
+    public void testSingleLongPathCompilation() {
+
+        String[] ingress = {"ingress"};
+        String egress = "egress";
+
+        MultiPointToSinglePointIntent intent = makeIntent(ingress, egress);
+        assertThat(intent, is(notNullValue()));
+
+        String[] hops = {"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8",
+                         egress};
+        MultiPointToSinglePointIntentCompiler compiler = makeCompiler(hops);
+        assertThat(compiler, is(notNullValue()));
+
+        List<Intent> result = compiler.compile(intent);
+        assertThat(result, is(Matchers.notNullValue()));
+        assertThat(result, hasSize(1));
+        Intent resultIntent = result.get(0);
+        assertThat(resultIntent instanceof LinkCollectionIntent, is(true));
+
+        if (resultIntent instanceof LinkCollectionIntent) {
+            LinkCollectionIntent linkIntent = (LinkCollectionIntent) resultIntent;
+            assertThat(linkIntent.links(), hasSize(9));
+            assertThat(linkIntent.links(), linksHasPath("ingress", "h1"));
+            assertThat(linkIntent.links(), linksHasPath("h1", "h2"));
+            assertThat(linkIntent.links(), linksHasPath("h2", "h3"));
+            assertThat(linkIntent.links(), linksHasPath("h4", "h5"));
+            assertThat(linkIntent.links(), linksHasPath("h5", "h6"));
+            assertThat(linkIntent.links(), linksHasPath("h7", "h8"));
+            assertThat(linkIntent.links(), linksHasPath("h8", "egress"));
+        }
+    }
+
+    /**
+     * Tests a simple topology where two ingress points share some path segments
+     * and some path segments are not shared.
+     */
+    @Test
+    public void testTwoIngressCompilation() {
+        String[] ingress = {"ingress1", "ingress2"};
+        String egress = "egress";
+
+        MultiPointToSinglePointIntent intent = makeIntent(ingress, egress);
+        assertThat(intent, is(notNullValue()));
+
+        final String[] hops = {"inner1", "inner2", egress};
+        MultiPointToSinglePointIntentCompiler compiler = makeCompiler(hops);
+        assertThat(compiler, is(notNullValue()));
+
+        List<Intent> result = compiler.compile(intent);
+        assertThat(result, is(notNullValue()));
+        assertThat(result, hasSize(1));
+        Intent resultIntent = result.get(0);
+        assertThat(resultIntent instanceof LinkCollectionIntent, is(true));
+
+        if (resultIntent instanceof LinkCollectionIntent) {
+            LinkCollectionIntent linkIntent = (LinkCollectionIntent) resultIntent;
+            assertThat(linkIntent.links(), hasSize(4));
+            assertThat(linkIntent.links(), linksHasPath("ingress1", "inner1"));
+            assertThat(linkIntent.links(), linksHasPath("ingress2", "inner1"));
+            assertThat(linkIntent.links(), linksHasPath("inner1", "inner2"));
+            assertThat(linkIntent.links(), linksHasPath("inner2", "egress"));
+        }
+    }
+
+    /**
+     * Tests a large number of ingress points that share a common path to the
+     * egress point.
+     */
+    @Test
+    public void testMultiIngressCompilation() {
+        String[] ingress = {"i1", "i2", "i3", "i4", "i5",
+                            "i6", "i7", "i8", "i9", "i10"};
+        String egress = "e";
+
+        MultiPointToSinglePointIntent intent = makeIntent(ingress, egress);
+        assertThat(intent, is(notNullValue()));
+
+        final String[] hops = {"n1", egress};
+        MultiPointToSinglePointIntentCompiler compiler = makeCompiler(hops);
+        assertThat(compiler, is(notNullValue()));
+
+        List<Intent> result = compiler.compile(intent);
+        assertThat(result, is(notNullValue()));
+        assertThat(result, hasSize(1));
+        Intent resultIntent = result.get(0);
+        assertThat(resultIntent instanceof LinkCollectionIntent, is(true));
+
+        if (resultIntent instanceof LinkCollectionIntent) {
+            LinkCollectionIntent linkIntent = (LinkCollectionIntent) resultIntent;
+            assertThat(linkIntent.links(), hasSize(ingress.length + 1));
+            for (String ingressToCheck : ingress) {
+                assertThat(linkIntent.links(),
+                           linksHasPath(ingressToCheck,
+                                        "n1"));
+            }
+            assertThat(linkIntent.links(), linksHasPath("n1", egress));
+        }
+    }
+}
diff --git a/core/pom.xml b/core/pom.xml
index afee0d0..c0f74cf 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -20,6 +20,7 @@
         <module>api</module>
         <module>net</module>
         <module>store</module>
+        <module>json</module>
     </modules>
 
     <dependencies>
diff --git a/core/store/dist/pom.xml b/core/store/dist/pom.xml
index 33517c7..b1e7172 100644
--- a/core/store/dist/pom.xml
+++ b/core/store/dist/pom.xml
@@ -19,21 +19,10 @@
     <dependencies>
         <dependency>
             <groupId>org.onlab.onos</groupId>
-            <artifactId>onos-api</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.onlab.onos</groupId>
             <artifactId>onos-core-serializers</artifactId>
             <version>${project.version}</version>
         </dependency>
 
-
-        <dependency>
-            <groupId>org.onlab.onos</groupId>
-            <artifactId>onlab-nio</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-        
         <dependency>
             <groupId>org.onlab.onos</groupId>
             <artifactId>onlab-netty</artifactId>
@@ -50,10 +39,6 @@
         </dependency>
 
         <dependency>
-            <groupId>org.apache.felix</groupId>
-            <artifactId>org.apache.felix.scr.annotations</artifactId>
-        </dependency>
-        <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava-testlib</artifactId>
             <scope>test</scope>
@@ -69,13 +54,4 @@
         </dependency>
     </dependencies>
 
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.felix</groupId>
-                <artifactId>maven-scr-plugin</artifactId>
-            </plugin>
-        </plugins>
-    </build>
-
 </project>
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
index c7852ae..30465ac 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
@@ -114,7 +114,7 @@
                     message.subject().value(), SERIALIZER.encode(message));
             return true;
         } catch (IOException e) {
-            log.error("Failed to send cluster message to nodeId: " + toNodeId, e);
+            log.trace("Failed to send cluster message to nodeId: " + toNodeId, e);
             throw e;
         }
     }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/common/impl/package-info.java b/core/store/dist/src/main/java/org/onlab/onos/store/common/impl/package-info.java
deleted file mode 100644
index 992fd49..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/common/impl/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-/**
- * Common abstractions and facilities for implementing distributed store
- * using gossip protocol.
- */
-package org.onlab.onos.store.common.impl;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/DeviceDescriptions.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/DeviceDescriptions.java
index 03c293a..43008d2 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/DeviceDescriptions.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/DeviceDescriptions.java
@@ -15,7 +15,7 @@
 import org.onlab.onos.net.device.DeviceDescription;
 import org.onlab.onos.net.device.PortDescription;
 import org.onlab.onos.store.Timestamp;
-import org.onlab.onos.store.common.impl.Timestamped;
+import org.onlab.onos.store.impl.Timestamped;
 
 /*
  * Collection of Description of a Device and Ports, given from a Provider.
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 12040dd..7ffcb3f 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
@@ -38,7 +38,7 @@
 import org.onlab.onos.store.cluster.messaging.ClusterMessage;
 import org.onlab.onos.store.cluster.messaging.ClusterMessageHandler;
 import org.onlab.onos.store.cluster.messaging.MessageSubject;
-import org.onlab.onos.store.common.impl.Timestamped;
+import org.onlab.onos.store.impl.Timestamped;
 import org.onlab.onos.store.serializers.KryoSerializer;
 import org.onlab.onos.store.serializers.DistributedStoreSerializers;
 import org.onlab.packet.ChassisId;
@@ -516,12 +516,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();
             }
         }
@@ -1139,7 +1139,7 @@
                 try {
                     unicastMessage(peer, DEVICE_ADVERTISE, ad);
                 } catch (IOException e) {
-                    log.error("Failed to send anti-entropy advertisement", e);
+                    log.debug("Failed to send anti-entropy advertisement to {}", peer);
                     return;
                 }
             } catch (Exception e) {
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InitDeviceDescs.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InitDeviceDescs.java
deleted file mode 100644
index 936723a..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InitDeviceDescs.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.onlab.onos.store.device.impl;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import org.apache.commons.lang3.concurrent.ConcurrentException;
-import org.apache.commons.lang3.concurrent.ConcurrentInitializer;
-import org.onlab.onos.net.device.DeviceDescription;
-import org.onlab.onos.store.common.impl.Timestamped;
-
-// FIXME: consider removing this class
-public final class InitDeviceDescs
-    implements ConcurrentInitializer<DeviceDescriptions> {
-
-    private final Timestamped<DeviceDescription> deviceDesc;
-
-    public InitDeviceDescs(Timestamped<DeviceDescription> deviceDesc) {
-        this.deviceDesc = checkNotNull(deviceDesc);
-    }
-    @Override
-    public DeviceDescriptions get() throws ConcurrentException {
-        return new DeviceDescriptions(deviceDesc);
-    }
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEvent.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEvent.java
index 344fe73..623a6b1 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEvent.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEvent.java
@@ -3,7 +3,7 @@
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.device.DeviceDescription;
 import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.common.impl.Timestamped;
+import org.onlab.onos.store.impl.Timestamped;
 
 import com.google.common.base.MoreObjects;
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEventSerializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEventSerializer.java
index 0d3d013..5f97afd 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEventSerializer.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalDeviceEventSerializer.java
@@ -3,7 +3,7 @@
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.device.DeviceDescription;
 import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.common.impl.Timestamped;
+import org.onlab.onos.store.impl.Timestamped;
 
 import com.esotericsoftware.kryo.Kryo;
 import com.esotericsoftware.kryo.Serializer;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEvent.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEvent.java
index d1fc73a..e036ec6 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEvent.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEvent.java
@@ -5,7 +5,7 @@
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.device.PortDescription;
 import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.common.impl.Timestamped;
+import org.onlab.onos.store.impl.Timestamped;
 
 import com.google.common.base.MoreObjects;
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEventSerializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEventSerializer.java
index 6fff395..4f4a9e6 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEventSerializer.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortEventSerializer.java
@@ -5,7 +5,7 @@
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.device.PortDescription;
 import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.common.impl.Timestamped;
+import org.onlab.onos.store.impl.Timestamped;
 
 import com.esotericsoftware.kryo.Kryo;
 import com.esotericsoftware.kryo.Serializer;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortStatusEvent.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortStatusEvent.java
index fd154da..ff59ab0 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortStatusEvent.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortStatusEvent.java
@@ -3,7 +3,7 @@
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.device.PortDescription;
 import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.common.impl.Timestamped;
+import org.onlab.onos.store.impl.Timestamped;
 
 import com.google.common.base.MoreObjects;
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortStatusEventSerializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortStatusEventSerializer.java
index 8f0c2b0..9d2db47 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortStatusEventSerializer.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/InternalPortStatusEventSerializer.java
@@ -3,7 +3,7 @@
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.device.PortDescription;
 import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.common.impl.Timestamped;
+import org.onlab.onos.store.impl.Timestamped;
 
 import com.esotericsoftware.kryo.Kryo;
 import com.esotericsoftware.kryo.Serializer;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/package-info.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/package-info.java
index c1f5aad..572948e 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/package-info.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/package-info.java
@@ -1,4 +1,4 @@
 /**
- * Implementation of device store using distributed distributed p2p synchronization protocol.
+ * Implementation of distributed device store using p2p synchronization protocol.
  */
 package org.onlab.onos.store.device.impl;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfo.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfo.java
new file mode 100644
index 0000000..47706f9
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfo.java
@@ -0,0 +1,54 @@
+package org.onlab.onos.store.flow;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.onlab.onos.cluster.NodeId;
+
+import com.google.common.base.Optional;
+
+/**
+ * Class to represent placement information about Master/Backup copy.
+ */
+public final class ReplicaInfo {
+
+    private final Optional<NodeId> master;
+    private final Collection<NodeId> backups;
+
+    /**
+     * Creates a ReplicaInfo instance.
+     *
+     * @param master NodeId of the node where the master copy should be
+     * @param backups collection of NodeId, where backup copies should be placed
+     */
+    public ReplicaInfo(NodeId master, Collection<NodeId> backups) {
+        this.master = Optional.fromNullable(master);
+        this.backups = checkNotNull(backups);
+    }
+
+    /**
+     * Returns the NodeId, if there is a Node where the master copy should be.
+     *
+     * @return NodeId, where the master copy should be placed
+     */
+    public Optional<NodeId> master() {
+        return master;
+    }
+
+    /**
+     * Returns the collection of NodeId, where backup copies should be placed.
+     *
+     * @return collection of NodeId, where backup copies should be placed
+     */
+    public Collection<NodeId> backups() {
+        return backups;
+    }
+
+    // for Serializer
+    private ReplicaInfo() {
+        this.master = Optional.absent();
+        this.backups = Collections.emptyList();
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfoEvent.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfoEvent.java
new file mode 100644
index 0000000..7cba127
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfoEvent.java
@@ -0,0 +1,49 @@
+package org.onlab.onos.store.flow;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.onlab.onos.event.AbstractEvent;
+import org.onlab.onos.net.DeviceId;
+
+/**
+ * Describes a device replicainfo event.
+ */
+public class ReplicaInfoEvent extends AbstractEvent<ReplicaInfoEvent.Type, DeviceId> {
+
+    private final ReplicaInfo replicaInfo;
+
+    /**
+     * Types of Replica info event.
+     */
+    public enum Type {
+        /**
+         * Event to notify that master placement should be changed.
+         */
+        MASTER_CHANGED,
+        //
+        // BACKUPS_CHANGED?
+    }
+
+
+    /**
+     * Creates an event of a given type and for the specified device,
+     * and replica info.
+     *
+     * @param type        replicainfo event type
+     * @param device      event device subject
+     * @param replicaInfo replicainfo
+     */
+    public ReplicaInfoEvent(Type type, DeviceId device, ReplicaInfo replicaInfo) {
+        super(type, device);
+        this.replicaInfo = checkNotNull(replicaInfo);
+    }
+
+    /**
+     * Returns the current replica information for the subject.
+     *
+     * @return replica information for the subject
+     */
+    public ReplicaInfo replicaInfo() {
+        return replicaInfo;
+    };
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfoEventListener.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfoEventListener.java
new file mode 100644
index 0000000..44239dc
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfoEventListener.java
@@ -0,0 +1,11 @@
+package org.onlab.onos.store.flow;
+
+import org.onlab.onos.event.EventListener;
+
+/**
+ * Entity capable of receiving Replica placement information-related events.
+ */
+public interface ReplicaInfoEventListener extends EventListener<ReplicaInfoEvent> {
+
+}
+
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfoService.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfoService.java
new file mode 100644
index 0000000..c3f1bbf
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/ReplicaInfoService.java
@@ -0,0 +1,18 @@
+package org.onlab.onos.store.flow;
+
+import org.onlab.onos.net.DeviceId;
+
+/**
+ * Service to return where the Replica should be placed.
+ */
+public interface ReplicaInfoService {
+
+    // returns where it should be.
+    /**
+     * Returns the placement information for given Device.
+     *
+     * @param deviceId identifier of the device
+     * @return placement information
+     */
+    ReplicaInfo getReplicaInfoFor(DeviceId deviceId);
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/ReplicaInfoManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/ReplicaInfoManager.java
new file mode 100644
index 0000000..0d4cd08
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/ReplicaInfoManager.java
@@ -0,0 +1,82 @@
+package org.onlab.onos.store.flow.impl;
+
+import static org.slf4j.LoggerFactory.getLogger;
+import static org.onlab.onos.store.flow.ReplicaInfoEvent.Type.MASTER_CHANGED;
+
+import java.util.Collections;
+import java.util.List;
+
+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.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.onos.cluster.NodeId;
+import org.onlab.onos.event.AbstractListenerRegistry;
+import org.onlab.onos.event.EventDeliveryService;
+import org.onlab.onos.mastership.MastershipEvent;
+import org.onlab.onos.mastership.MastershipListener;
+import org.onlab.onos.mastership.MastershipService;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.store.flow.ReplicaInfo;
+import org.onlab.onos.store.flow.ReplicaInfoEvent;
+import org.onlab.onos.store.flow.ReplicaInfoEventListener;
+import org.onlab.onos.store.flow.ReplicaInfoService;
+import org.slf4j.Logger;
+
+/**
+ * Manages replica placement information.
+ */
+@Component(immediate = true)
+@Service
+public class ReplicaInfoManager implements ReplicaInfoService {
+
+    private final Logger log = getLogger(getClass());
+
+    private final MastershipListener mastershipListener = new InternalMastershipListener();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected EventDeliveryService eventDispatcher;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MastershipService mastershipService;
+
+    protected final AbstractListenerRegistry<ReplicaInfoEvent, ReplicaInfoEventListener>
+        listenerRegistry = new AbstractListenerRegistry<>();
+
+    @Activate
+    public void activate() {
+        eventDispatcher.addSink(ReplicaInfoEvent.class, listenerRegistry);
+        mastershipService.addListener(mastershipListener);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        eventDispatcher.removeSink(ReplicaInfoEvent.class);
+        mastershipService.removeListener(mastershipListener);
+        log.info("Stopped");
+    }
+
+    @Override
+    public ReplicaInfo getReplicaInfoFor(DeviceId deviceId) {
+        // TODO: populate backup List when we reach the point we need them.
+        return new ReplicaInfo(mastershipService.getMasterFor(deviceId),
+                               Collections.<NodeId>emptyList());
+    }
+
+    final class InternalMastershipListener implements MastershipListener {
+
+        @Override
+        public void event(MastershipEvent event) {
+            // TODO: distinguish stby list update, when MastershipService,
+            //       start publishing them
+            final List<NodeId> standbyList = Collections.<NodeId>emptyList();
+            eventDispatcher.post(new ReplicaInfoEvent(MASTER_CHANGED,
+                                event.subject(),
+                                new ReplicaInfo(event.master(), standbyList)));
+        }
+    }
+
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/package-info.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/package-info.java
new file mode 100644
index 0000000..cedf68e
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Implementation of the distributed flow rule store using p2p synchronization
+ * protocol.
+ */
+package org.onlab.onos.store.flow.impl;
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..f43ae8e 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;
@@ -32,7 +38,7 @@
 import org.onlab.onos.store.cluster.messaging.ClusterMessage;
 import org.onlab.onos.store.cluster.messaging.ClusterMessageHandler;
 import org.onlab.onos.store.cluster.messaging.MessageSubject;
-import org.onlab.onos.store.common.impl.Timestamped;
+import org.onlab.onos.store.impl.Timestamped;
 import org.onlab.onos.store.serializers.DistributedStoreSerializers;
 import org.onlab.onos.store.serializers.KryoSerializer;
 import org.onlab.packet.IpPrefix;
@@ -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 to {}", peer);
+                    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..a5f7727
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Implementation of the distributed host store using p2p synchronization protocol.
+ */
+package org.onlab.onos.store.host.impl;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/common/impl/Timestamped.java b/core/store/dist/src/main/java/org/onlab/onos/store/impl/Timestamped.java
similarity index 88%
rename from core/store/dist/src/main/java/org/onlab/onos/store/common/impl/Timestamped.java
rename to core/store/dist/src/main/java/org/onlab/onos/store/impl/Timestamped.java
index 8d2aee1..ae79831 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/common/impl/Timestamped.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/impl/Timestamped.java
@@ -1,4 +1,4 @@
-package org.onlab.onos.store.common.impl;
+package org.onlab.onos.store.impl;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
@@ -58,12 +58,12 @@
     }
 
     /**
-     * Tests if this timestamp is newer thatn the specified timestamp.
-     * @param timestamp to compare agains
+     * Tests if this timestamp is newer than the specified timestamp.
+     * @param other timestamp to compare against
      * @return true if this instance is newer
      */
-    public boolean isNewer(Timestamp timestamp) {
-        return this.timestamp.compareTo(checkNotNull(timestamp)) > 0;
+    public boolean isNewer(Timestamp other) {
+        return this.timestamp.compareTo(checkNotNull(other)) > 0;
     }
 
     @Override
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..f9cb71c 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;
@@ -41,7 +39,7 @@
 import org.onlab.onos.store.cluster.messaging.ClusterMessage;
 import org.onlab.onos.store.cluster.messaging.ClusterMessageHandler;
 import org.onlab.onos.store.cluster.messaging.MessageSubject;
-import org.onlab.onos.store.common.impl.Timestamped;
+import org.onlab.onos.store.impl.Timestamped;
 import org.onlab.onos.store.serializers.DistributedStoreSerializers;
 import org.onlab.onos.store.serializers.KryoSerializer;
 import org.onlab.util.KryoPool;
@@ -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/dist/src/main/java/org/onlab/onos/store/link/impl/InternalLinkEvent.java b/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/InternalLinkEvent.java
index 9bb3445..74694ba 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/InternalLinkEvent.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/InternalLinkEvent.java
@@ -4,7 +4,7 @@
 
 import org.onlab.onos.net.link.LinkDescription;
 import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.store.common.impl.Timestamped;
+import org.onlab.onos.store.impl.Timestamped;
 
 /**
  * Information published by GossipDeviceStore to notify peers of a device
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/package-info.java b/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/package-info.java
index c675f84..ab45e4f 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/package-info.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/package-info.java
@@ -1,4 +1,4 @@
 /**
- * Implementation of link store using distributed p2p synchronization protocol.
+ * Implementation of distributed link store using p2p synchronization protocol.
  */
 package org.onlab.onos.store.link.impl;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/serializers/DistributedStoreSerializers.java b/core/store/dist/src/main/java/org/onlab/onos/store/serializers/DistributedStoreSerializers.java
index a04539b..114150f 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/serializers/DistributedStoreSerializers.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/serializers/DistributedStoreSerializers.java
@@ -1,7 +1,7 @@
 package org.onlab.onos.store.serializers;
 
-import org.onlab.onos.store.common.impl.Timestamped;
 import org.onlab.onos.store.impl.MastershipBasedTimestamp;
+import org.onlab.onos.store.impl.Timestamped;
 import org.onlab.onos.store.impl.WallClockTimestamp;
 import org.onlab.util.KryoPool;
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/topology/impl/package-info.java b/core/store/dist/src/main/java/org/onlab/onos/store/topology/impl/package-info.java
new file mode 100644
index 0000000..ec719d0
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/topology/impl/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Implementation of distributed topology store using p2p synchronization protocol.
+ */
+package org.onlab.onos.store.topology.impl;
diff --git a/core/store/dist/src/test/java/org/onlab/onos/store/common/impl/TimestampedTest.java b/core/store/dist/src/test/java/org/onlab/onos/store/impl/TimestampedTest.java
similarity index 96%
rename from core/store/dist/src/test/java/org/onlab/onos/store/common/impl/TimestampedTest.java
rename to core/store/dist/src/test/java/org/onlab/onos/store/impl/TimestampedTest.java
index deb3e4d..3e7fec2 100644
--- a/core/store/dist/src/test/java/org/onlab/onos/store/common/impl/TimestampedTest.java
+++ b/core/store/dist/src/test/java/org/onlab/onos/store/impl/TimestampedTest.java
@@ -1,4 +1,4 @@
-package org.onlab.onos.store.common.impl;
+package org.onlab.onos.store.impl;
 
 import static org.junit.Assert.*;
 
@@ -6,7 +6,6 @@
 
 import org.junit.Test;
 import org.onlab.onos.store.Timestamp;
-import org.onlab.onos.store.impl.MastershipBasedTimestamp;
 import org.onlab.util.KryoPool;
 
 import com.google.common.testing.EqualsTester;
diff --git a/core/store/hz/cluster/pom.xml b/core/store/hz/cluster/pom.xml
index d183aed..4ab347e 100644
--- a/core/store/hz/cluster/pom.xml
+++ b/core/store/hz/cluster/pom.xml
@@ -19,10 +19,6 @@
     <dependencies>
         <dependency>
             <groupId>org.onlab.onos</groupId>
-            <artifactId>onos-api</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.onlab.onos</groupId>
             <artifactId>onos-core-serializers</artifactId>
             <version>${project.version}</version>
         </dependency>
@@ -38,23 +34,6 @@
             <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/common/pom.xml b/core/store/hz/common/pom.xml
index 1d79206..a4ff179 100644
--- a/core/store/hz/common/pom.xml
+++ b/core/store/hz/common/pom.xml
@@ -19,34 +19,13 @@
     <dependencies>
         <dependency>
             <groupId>org.onlab.onos</groupId>
-            <artifactId>onos-api</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.onlab.onos</groupId>
             <artifactId>onos-core-serializers</artifactId>
             <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>
-        <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</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/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/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..dae40d4 100644
--- a/core/store/hz/pom.xml
+++ b/core/store/hz/pom.xml
@@ -17,35 +17,13 @@
     <modules>
         <module>common</module>
         <module>cluster</module>
-        <module>net</module>
     </modules>
 
     <dependencies>
         <dependency>
-            <groupId>com.google.guava</groupId>
-            <artifactId>guava</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.onlab.onos</groupId>
-            <artifactId>onlab-misc</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.onlab.onos</groupId>
-            <artifactId>onlab-junit</artifactId>
-        </dependency>
-        <dependency>
           <groupId>com.hazelcast</groupId>
           <artifactId>hazelcast</artifactId>
         </dependency>
     </dependencies>
 
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.felix</groupId>
-                <artifactId>maven-bundle-plugin</artifactId>
-            </plugin>
-        </plugins>
-    </build>
-
 </project>
diff --git a/core/store/pom.xml b/core/store/pom.xml
index b94b4fe..47e3162 100644
--- a/core/store/pom.xml
+++ b/core/store/pom.xml
@@ -1,5 +1,7 @@
 <?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">
+<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>
@@ -19,24 +21,17 @@
         <module>dist</module>
         <module>hz</module>
         <module>serializers</module>
-  </modules>
+    </modules>
 
     <dependencies>
         <dependency>
-            <groupId>com.google.guava</groupId>
-            <artifactId>guava</artifactId>
-        </dependency>
-        <dependency>
             <groupId>org.onlab.onos</groupId>
-            <artifactId>onlab-misc</artifactId>
+            <artifactId>onos-api</artifactId>
         </dependency>
+
         <dependency>
-            <groupId>org.onlab.onos</groupId>
-            <artifactId>onlab-junit</artifactId>
-        </dependency>
-        <dependency>
-          <groupId>com.hazelcast</groupId>
-          <artifactId>hazelcast</artifactId>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr.annotations</artifactId>
         </dependency>
     </dependencies>
 
@@ -44,7 +39,7 @@
         <plugins>
             <plugin>
                 <groupId>org.apache.felix</groupId>
-                <artifactId>maven-bundle-plugin</artifactId>
+                <artifactId>maven-scr-plugin</artifactId>
             </plugin>
         </plugins>
     </build>
diff --git a/core/store/serializers/pom.xml b/core/store/serializers/pom.xml
index fe0a501..5f92ad1 100644
--- a/core/store/serializers/pom.xml
+++ b/core/store/serializers/pom.xml
@@ -18,14 +18,6 @@
 
     <dependencies>
         <dependency>
-            <groupId>org.onlab.onos</groupId>
-            <artifactId>onos-api</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.felix</groupId>
-            <artifactId>org.apache.felix.scr.annotations</artifactId>
-        </dependency>
-        <dependency>
             <groupId>com.esotericsoftware</groupId>
             <artifactId>kryo</artifactId>
         </dependency>
@@ -36,13 +28,4 @@
         </dependency>
     </dependencies>
 
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.felix</groupId>
-                <artifactId>maven-scr-plugin</artifactId>
-            </plugin>
-        </plugins>
-    </build>
-
 </project>
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 1d9d8dc..b63f844 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,6 +17,8 @@
 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.MastershipRole;
@@ -24,16 +26,21 @@
 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.ChassisId;
 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 {
 
@@ -43,6 +50,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
@@ -53,6 +62,7 @@
             .register(MISC)
             .register(ImmutableMap.class, new ImmutableMapSerializer())
             .register(ImmutableList.class, new ImmutableListSerializer())
+            .register(ImmutableSet.class, new ImmutableSetSerializer())
             .register(
                     //
                     ArrayList.class,
@@ -73,8 +83,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())
@@ -87,6 +99,7 @@
             .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/pom.xml b/core/store/trivial/pom.xml
index b35f6a50..d9f9154 100644
--- a/core/store/trivial/pom.xml
+++ b/core/store/trivial/pom.xml
@@ -18,26 +18,9 @@
 
     <dependencies>
         <dependency>
-            <groupId>org.onlab.onos</groupId>
-            <artifactId>onos-api</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.felix</groupId>
-            <artifactId>org.apache.felix.scr.annotations</artifactId>
-        </dependency>
-        <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-lang3</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/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
index 3fefbfd..fbfaf9d 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
@@ -5,8 +5,6 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 
-import org.apache.commons.lang3.concurrent.ConcurrentException;
-import org.apache.commons.lang3.concurrent.ConcurrentInitializer;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -36,6 +34,7 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -72,8 +71,7 @@
     public static final String DEVICE_NOT_FOUND = "Device with ID %s not found";
 
     // collection of Description given from various providers
-    private final ConcurrentMap<DeviceId,
-                            ConcurrentMap<ProviderId, DeviceDescriptions>>
+    private final ConcurrentMap<DeviceId, Map<ProviderId, DeviceDescriptions>>
                                 deviceDescs = Maps.newConcurrentMap();
 
     // cache of Device and Ports generated by compositing descriptions from providers
@@ -118,15 +116,16 @@
                                      DeviceId deviceId,
                                      DeviceDescription deviceDescription) {
 
-        ConcurrentMap<ProviderId, DeviceDescriptions> providerDescs
-            = getDeviceDescriptions(deviceId);
+        Map<ProviderId, DeviceDescriptions> providerDescs
+            = getOrCreateDeviceDescriptions(deviceId);
 
         synchronized (providerDescs) {
             // locking per device
 
             DeviceDescriptions descs
-                = createIfAbsentUnchecked(providerDescs, providerId,
-                        new InitDeviceDescs(deviceDescription));
+                = getOrCreateProviderDeviceDescriptions(providerDescs,
+                                                        providerId,
+                                                        deviceDescription);
 
             Device oldDevice = devices.get(deviceId);
             // update description
@@ -193,8 +192,8 @@
 
     @Override
     public DeviceEvent markOffline(DeviceId deviceId) {
-        ConcurrentMap<ProviderId, DeviceDescriptions> providerDescs
-            = getDeviceDescriptions(deviceId);
+        Map<ProviderId, DeviceDescriptions> providerDescs
+            = getOrCreateDeviceDescriptions(deviceId);
 
         // locking device
         synchronized (providerDescs) {
@@ -219,7 +218,7 @@
         Device device = devices.get(deviceId);
         checkArgument(device != null, DEVICE_NOT_FOUND, deviceId);
 
-        ConcurrentMap<ProviderId, DeviceDescriptions> descsMap = deviceDescs.get(deviceId);
+        Map<ProviderId, DeviceDescriptions> descsMap = deviceDescs.get(deviceId);
         checkArgument(descsMap != null, DEVICE_NOT_FOUND, deviceId);
 
         List<DeviceEvent> events = new ArrayList<>();
@@ -288,12 +287,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();
             }
         }
@@ -307,10 +306,36 @@
                 NewConcurrentHashMap.<PortNumber, Port>ifNeeded());
     }
 
-    private ConcurrentMap<ProviderId, DeviceDescriptions> getDeviceDescriptions(
+    private Map<ProviderId, DeviceDescriptions> getOrCreateDeviceDescriptions(
             DeviceId deviceId) {
-        return createIfAbsentUnchecked(deviceDescs, deviceId,
-                NewConcurrentHashMap.<ProviderId, DeviceDescriptions>ifNeeded());
+        Map<ProviderId, DeviceDescriptions> r;
+        r = deviceDescs.get(deviceId);
+        if (r != null) {
+            return r;
+        }
+        r = new HashMap<>();
+        final Map<ProviderId, DeviceDescriptions> concurrentlyAdded;
+        concurrentlyAdded = deviceDescs.putIfAbsent(deviceId, r);
+        if (concurrentlyAdded != null) {
+            return concurrentlyAdded;
+        } else {
+            return r;
+        }
+    }
+
+    // Guarded by deviceDescs value (=Device lock)
+    private DeviceDescriptions getOrCreateProviderDeviceDescriptions(
+                            Map<ProviderId, DeviceDescriptions> device,
+                            ProviderId providerId, DeviceDescription deltaDesc) {
+
+        synchronized (device) {
+            DeviceDescriptions r = device.get(providerId);
+            if (r == null) {
+                r = new DeviceDescriptions(deltaDesc);
+                device.put(providerId, r);
+            }
+            return r;
+        }
     }
 
     @Override
@@ -319,12 +344,12 @@
         Device device = devices.get(deviceId);
         checkArgument(device != null, DEVICE_NOT_FOUND, deviceId);
 
-        ConcurrentMap<ProviderId, DeviceDescriptions> descsMap = deviceDescs.get(deviceId);
+        Map<ProviderId, DeviceDescriptions> descsMap = deviceDescs.get(deviceId);
         checkArgument(descsMap != null, DEVICE_NOT_FOUND, deviceId);
 
         synchronized (descsMap) {
             DeviceDescriptions descs = descsMap.get(providerId);
-            // assuming all providers must to give DeviceDescription
+            // assuming all providers must give DeviceDescription first
             checkArgument(descs != null,
                     "Device description for Device ID %s from Provider %s was not found",
                     deviceId, providerId);
@@ -368,7 +393,7 @@
 
     @Override
     public DeviceEvent removeDevice(DeviceId deviceId) {
-        ConcurrentMap<ProviderId, DeviceDescriptions> descs = getDeviceDescriptions(deviceId);
+        Map<ProviderId, DeviceDescriptions> descs = getOrCreateDeviceDescriptions(deviceId);
         synchronized (descs) {
             Device device = devices.remove(deviceId);
             // should DEVICE_REMOVED carry removed ports?
@@ -391,7 +416,7 @@
      * @return Device instance
      */
     private Device composeDevice(DeviceId deviceId,
-            ConcurrentMap<ProviderId, DeviceDescriptions> providerDescs) {
+            Map<ProviderId, DeviceDescriptions> providerDescs) {
 
         checkArgument(!providerDescs.isEmpty(), "No Device descriptions supplied");
 
@@ -432,14 +457,14 @@
      *
      * @param device device the port is on
      * @param number port number
-     * @param providerDescs Collection of Descriptions from multiple providers
+     * @param descsMap Collection of Descriptions from multiple providers
      * @return Port instance
      */
     private Port composePort(Device device, PortNumber number,
-                ConcurrentMap<ProviderId, DeviceDescriptions> providerDescs) {
+                Map<ProviderId, DeviceDescriptions> descsMap) {
 
-        ProviderId primary = pickPrimaryPID(providerDescs);
-        DeviceDescriptions primDescs = providerDescs.get(primary);
+        ProviderId primary = pickPrimaryPID(descsMap);
+        DeviceDescriptions primDescs = descsMap.get(primary);
         // if no primary, assume not enabled
         // TODO: revisit this default port enabled/disabled behavior
         boolean isEnabled = false;
@@ -451,7 +476,7 @@
             annotations = merge(annotations, portDesc.annotations());
         }
 
-        for (Entry<ProviderId, DeviceDescriptions> e : providerDescs.entrySet()) {
+        for (Entry<ProviderId, DeviceDescriptions> e : descsMap.entrySet()) {
             if (e.getKey().equals(primary)) {
                 continue;
             }
@@ -473,10 +498,9 @@
     /**
      * @return primary ProviderID, or randomly chosen one if none exists
      */
-    private ProviderId pickPrimaryPID(
-            ConcurrentMap<ProviderId, DeviceDescriptions> providerDescs) {
+    private ProviderId pickPrimaryPID(Map<ProviderId, DeviceDescriptions> descsMap) {
         ProviderId fallBackPrimary = null;
-        for (Entry<ProviderId, DeviceDescriptions> e : providerDescs.entrySet()) {
+        for (Entry<ProviderId, DeviceDescriptions> e : descsMap.entrySet()) {
             if (!e.getKey().isAncillary()) {
                 return e.getKey();
             } else if (fallBackPrimary == null) {
@@ -487,21 +511,6 @@
         return fallBackPrimary;
     }
 
-    public static final class InitDeviceDescs
-        implements ConcurrentInitializer<DeviceDescriptions> {
-
-        private final DeviceDescription deviceDesc;
-
-        public InitDeviceDescs(DeviceDescription deviceDesc) {
-            this.deviceDesc = checkNotNull(deviceDesc);
-        }
-        @Override
-        public DeviceDescriptions get() throws ConcurrentException {
-            return new DeviceDescriptions(deviceDesc);
-        }
-    }
-
-
     /**
      * Collection of Description of a Device and it's Ports given from a Provider.
      */
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/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkStore.java
index bcddda3..a9a0982 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkStore.java
@@ -1,12 +1,10 @@
 package org.onlab.onos.store.trivial.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.SetMultimap;
 
-import org.apache.commons.lang3.concurrent.ConcurrentUtils;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -20,7 +18,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.link.DefaultLinkDescription;
 import org.onlab.onos.net.link.LinkDescription;
 import org.onlab.onos.net.link.LinkEvent;
@@ -28,11 +25,12 @@
 import org.onlab.onos.net.link.LinkStoreDelegate;
 import org.onlab.onos.net.provider.ProviderId;
 import org.onlab.onos.store.AbstractStore;
-import org.onlab.util.NewConcurrentHashMap;
 import org.slf4j.Logger;
 
 import java.util.Collections;
+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;
@@ -47,6 +45,7 @@
 import static org.slf4j.LoggerFactory.getLogger;
 import static com.google.common.collect.Multimaps.synchronizedSetMultimap;
 import static com.google.common.base.Predicates.notNull;
+import static com.google.common.base.Verify.verifyNotNull;
 
 /**
  * Manages inventory of infrastructure links using trivial in-memory structures
@@ -61,8 +60,7 @@
     private final Logger log = getLogger(getClass());
 
     // Link inventory
-    private final ConcurrentMap<LinkKey,
-            ConcurrentMap<ProviderId, LinkDescription>>
+    private final ConcurrentMap<LinkKey, Map<ProviderId, LinkDescription>>
                     linkDescs = new ConcurrentHashMap<>();
 
     // Link instance cache
@@ -151,7 +149,7 @@
                                         LinkDescription linkDescription) {
         LinkKey key = linkKey(linkDescription.src(), linkDescription.dst());
 
-        ConcurrentMap<ProviderId, LinkDescription> descs = getLinkDescriptions(key);
+        Map<ProviderId, LinkDescription> descs = getOrCreateLinkDescriptions(key);
         synchronized (descs) {
             final Link oldLink = links.get(key);
             // update description
@@ -166,7 +164,7 @@
 
     // Guarded by linkDescs value (=locking each Link)
     private LinkDescription createOrUpdateLinkDescription(
-                             ConcurrentMap<ProviderId, LinkDescription> descs,
+                             Map<ProviderId, LinkDescription> descs,
                              ProviderId providerId,
                              LinkDescription linkDescription) {
 
@@ -227,7 +225,7 @@
     @Override
     public LinkEvent removeLink(ConnectPoint src, ConnectPoint dst) {
         final LinkKey key = linkKey(src, dst);
-        ConcurrentMap<ProviderId, LinkDescription> descs = getLinkDescriptions(key);
+        Map<ProviderId, LinkDescription> descs = getOrCreateLinkDescriptions(key);
         synchronized (descs) {
             Link link = links.remove(key);
             descs.clear();
@@ -247,8 +245,8 @@
     /**
      * @return primary ProviderID, or randomly chosen one if none exists
      */
-    private ProviderId pickPrimaryPID(
-            ConcurrentMap<ProviderId, LinkDescription> providerDescs) {
+    // Guarded by linkDescs value (=locking each Link)
+    private ProviderId getBaseProviderId(Map<ProviderId, LinkDescription> providerDescs) {
 
         ProviderId fallBackPrimary = null;
         for (Entry<ProviderId, LinkDescription> e : providerDescs.entrySet()) {
@@ -262,9 +260,10 @@
         return fallBackPrimary;
     }
 
-    private Link composeLink(ConcurrentMap<ProviderId, LinkDescription> descs) {
-        ProviderId primary = pickPrimaryPID(descs);
-        LinkDescription base = descs.get(primary);
+    // Guarded by linkDescs value (=locking each Link)
+    private Link composeLink(Map<ProviderId, LinkDescription> descs) {
+        ProviderId primary = getBaseProviderId(descs);
+        LinkDescription base = descs.get(verifyNotNull(primary));
 
         ConnectPoint src = base.src();
         ConnectPoint dst = base.dst();
@@ -289,9 +288,20 @@
         return new DefaultLink(primary , src, dst, type, annotations);
     }
 
-    private ConcurrentMap<ProviderId, LinkDescription> getLinkDescriptions(LinkKey key) {
-        return ConcurrentUtils.createIfAbsentUnchecked(linkDescs, key,
-                NewConcurrentHashMap.<ProviderId, LinkDescription>ifNeeded());
+    private Map<ProviderId, LinkDescription> getOrCreateLinkDescriptions(LinkKey key) {
+        Map<ProviderId, LinkDescription> r;
+        r = linkDescs.get(key);
+        if (r != null) {
+            return r;
+        }
+        r = new HashMap<>();
+        final Map<ProviderId, LinkDescription> concurrentlyAdded;
+        concurrentlyAdded = linkDescs.putIfAbsent(key, r);
+        if (concurrentlyAdded == null) {
+            return r;
+        } else {
+            return concurrentlyAdded;
+        }
     }
 
     private final Function<LinkKey, Link> lookupLink = new LookupLink();
@@ -302,20 +312,11 @@
     private final class LookupLink implements Function<LinkKey, Link> {
         @Override
         public Link apply(LinkKey input) {
-            return links.get(input);
-        }
-    }
-
-    private static final Predicate<Provided> IS_PRIMARY = new IsPrimary();
-    private static final Predicate<Provided> isPrimary() {
-        return IS_PRIMARY;
-    }
-
-    private static final class IsPrimary implements Predicate<Provided> {
-
-        @Override
-        public boolean apply(Provided input) {
-            return !input.providerId().isAncillary();
+            if (input == null) {
+                return null;
+            } else {
+                return links.get(input);
+            }
         }
     }
 }
diff --git a/features/features.xml b/features/features.xml
index cf4e34d..b66a56a 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <features xmlns="http://karaf.apache.org/xmlns/features/v1.2.0"
           name="onos-1.0.0">
-    <repository>mvn:org.onlab.onos/onos-features/1.0.0-SNAPSHOT/xml/features</repository>
+    <repository>mvn:org.onlab.onos/onos-features/1.0.0-SNAPSHOT/xml/features
+    </repository>
 
     <feature name="onos-thirdparty-base" version="1.0.0"
              description="ONOS 3rd party dependencies">
@@ -28,20 +29,22 @@
 
         <bundle>mvn:org.onlab.onos/onlab-nio/1.0.0-SNAPSHOT</bundle>
 
-	<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.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"
              description="ONOS 3rd party dependencies">
         <feature>war</feature>
         <bundle>mvn:com.fasterxml.jackson.core/jackson-core/2.4.2</bundle>
-        <bundle>mvn:com.fasterxml.jackson.core/jackson-annotations/2.4.2</bundle>
+        <bundle>mvn:com.fasterxml.jackson.core/jackson-annotations/2.4.2
+        </bundle>
         <bundle>mvn:com.fasterxml.jackson.core/jackson-databind/2.4.2</bundle>
         <bundle>mvn:com.sun.jersey/jersey-core/1.18.1</bundle>
         <bundle>mvn:com.sun.jersey/jersey-server/1.18.1</bundle>
         <bundle>mvn:com.sun.jersey/jersey-servlet/1.18.1</bundle>
-        
+
     </feature>
 
     <feature name="onos-api" version="1.0.0"
@@ -95,7 +98,7 @@
     </feature>
 
     <feature name="onos-openflow" version="1.0.0"
-            description="ONOS OpenFlow API, Controller &amp; Providers">
+             description="ONOS OpenFlow API, Controller &amp; Providers">
         <feature>onos-api</feature>
         <bundle>mvn:io.netty/netty/3.9.2.Final</bundle>
         <bundle>mvn:org.onlab.onos/onos-of-api/1.0.0-SNAPSHOT</bundle>
@@ -159,4 +162,11 @@
         <bundle>mvn:org.onlab.onos/onos-app-sdnip/1.0.0-SNAPSHOT</bundle>
     </feature>
 
+    <feature name="onos-app-calendar" version="1.0.0"
+             description="REST interface for scheduling intents from an external calendar">
+        <feature>onos-api</feature>
+        <feature>onos-thirdparty-web</feature>
+        <bundle>mvn:org.onlab.onos/onos-app-calendar/1.0.0-SNAPSHOT</bundle>
+    </feature>
+
 </features>
diff --git a/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFOpticalSwitchImplLINC13.java b/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFOpticalSwitchImplLINC13.java
index 71a7ab8..79a162e 100644
--- a/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFOpticalSwitchImplLINC13.java
+++ b/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFOpticalSwitchImplLINC13.java
@@ -5,6 +5,7 @@
 import org.onlab.onos.openflow.controller.driver.SwitchDriverSubHandshakeNotStarted;
 import org.onlab.onos.openflow.controller.Dpid;
 import org.onlab.onos.openflow.controller.driver.AbstractOpenFlowSwitch;
+import org.projectfloodlight.openflow.protocol.OFBarrierRequest;
 import org.projectfloodlight.openflow.protocol.OFCircuitPortsReply;
 import org.projectfloodlight.openflow.protocol.OFCircuitPortsRequest;
 import org.projectfloodlight.openflow.protocol.OFDescStatsReply;
@@ -21,7 +22,6 @@
 import org.projectfloodlight.openflow.protocol.oxm.OFOxmOchSigid;
 import org.projectfloodlight.openflow.protocol.oxm.OFOxmOchSigidBasic;
 import org.projectfloodlight.openflow.protocol.oxm.OFOxmOchSigtype;
-import org.projectfloodlight.openflow.protocol.oxm.OFOxmOchSigtypeBasic;
 import org.projectfloodlight.openflow.types.CircuitSignalID;
 import org.projectfloodlight.openflow.types.OFPort;
 import org.projectfloodlight.openflow.types.U8;
@@ -119,11 +119,12 @@
                 processHandshakeOFExperimenterPortDescRequest(
                         (OFCircuitPortsReply) m);
                 driverHandshakeComplete.set(true);
-               /* try {
+                try {
                     testMA();
+                    testReverseMA();
                 } catch (IOException e) {
                     e.printStackTrace();
-                }*/
+                }
                 break;
             default:
                 log.debug("Received message {} during switch-driver " +
@@ -163,32 +164,23 @@
                           "message " +
                           "{}",
                   circuitPortsRequest.toString());
-        channel.write(Collections.singletonList(circuitPortsRequest));
+        sendMsg(Collections.<OFMessage>singletonList(circuitPortsRequest));
     }
 
 
-
-    //todo for testing
-    public static final U8 SIGNAL_TYPE = U8.of((short) 1);
+    public static final U8 SIGNAL_TYPE = U8.of((short) 10);
     private void testMA() throws IOException {
         log.debug("LINC OE *** Testing MA ");
-        short lambda = 100;
-        if (getId() == 0x0000ffffffffff02L) {
+        short lambda = 1;
+        if (getId() == 0x0000ffffffffff01L) {
             final int inport = 10;
             final int outport = 20;
             //Circuit signal id
             CircuitSignalID sigID = getSignalID(lambda);
 
-            OFOxmOchSigid fieldSigIDMatch = factory().oxms().ochSigid(sigID);
-            OFOxmOchSigtype fieldSigType = factory()
-                    .oxms()
-                    .ochSigtype(SIGNAL_TYPE);
-
             OFOxmOchSigidBasic ofOxmOchSigidBasic =
                     factory().oxms().ochSigidBasic(sigID);
 
-            OFOxmOchSigtypeBasic ofOxmOchSigtypeBasic =
-                    factory().oxms().ochSigtypeBasic(SIGNAL_TYPE);
 
             //Match Port
             OFOxmInPort fieldPort = factory().oxms()
@@ -196,27 +188,21 @@
             OFMatchV3 matchPort =
                     factory()
                             .buildMatchV3().
-                            setOxmList(OFOxmList.of(fieldPort,
-                                                    fieldSigType,
-                                                    fieldSigIDMatch)).build();
+                            setOxmList(OFOxmList.of(fieldPort)).build();
 
 
             // Set Action outport ,sigType and sigID
             List<OFAction> actionList = new ArrayList<>();
             OFAction actionOutPort =
                     factory().actions().output(OFPort.of(outport),
-                                                  Short.MAX_VALUE);
+                                                  0xffff);
 
             OFActionCircuit actionCircuit = factory()
                     .actions()
                     .circuit(ofOxmOchSigidBasic);
-            OFActionCircuit setActionSigType = factory()
-                    .actions()
-                    .circuit(ofOxmOchSigtypeBasic);
 
-            actionList.add(actionOutPort);
-            actionList.add(setActionSigType);
             actionList.add(actionCircuit);
+            actionList.add(actionOutPort);
 
             OFInstruction instructionAction =
                     factory().instructions().buildApplyActions()
@@ -228,6 +214,7 @@
             OFMessage opticalFlowEntry =
                     factory().buildFlowAdd()
                                 .setMatch(matchPort)
+                                .setPriority(100)
                                 .setInstructions(instructions)
                                 .setXid(getNextTransactionId())
                                 .build();
@@ -235,9 +222,10 @@
             List<OFMessage> msglist = new ArrayList<>(1);
             msglist.add(opticalFlowEntry);
             write(msglist);
+            sendBarrier(true);
         } else if (getId() == 0x0000ffffffffff03L) {
-            final int inport = 21;
-            final int outport = 22;
+            final int inport = 30;
+            final int outport = 31;
             //Circuit signal id
             CircuitSignalID sigID = getSignalID(lambda);
 
@@ -249,9 +237,6 @@
             OFOxmOchSigidBasic ofOxmOchSigidBasic =
                     factory().oxms().ochSigidBasic(sigID);
 
-            OFOxmOchSigtypeBasic ofOxmOchSigtypeBasic =
-                    factory().oxms().ochSigtypeBasic(SIGNAL_TYPE);
-
             //Match Port,SigType,SigID
             OFOxmInPort fieldPort = factory()
                     .oxms()
@@ -259,27 +244,26 @@
             OFMatchV3 matchPort = factory()
                     .buildMatchV3()
                     .setOxmList(OFOxmList.of(fieldPort,
-                                             fieldSigType,
-                                             fieldSigIDMatch))
+                                             fieldSigIDMatch,
+                                             fieldSigType
+                    ))
                     .build();
 
             // Set Action outport ,SigType, sigID
             List<OFAction> actionList = new ArrayList<>();
             OFAction actionOutPort =
                     factory().actions().output(OFPort.of(outport),
-                                                  Short.MAX_VALUE);
+                                                  0xffff);
 
-            OFActionCircuit setActionSigType = factory()
-                    .actions()
-                    .circuit(ofOxmOchSigtypeBasic);
             OFActionCircuit actionCircuit = factory()
                     .actions()
                     .circuit(ofOxmOchSigidBasic);
 
 
-            actionList.add(actionOutPort);
-            actionList.add(setActionSigType);
+
+            //actionList.add(setActionSigType);
             actionList.add(actionCircuit);
+            actionList.add(actionOutPort);
 
             OFInstruction instructionAction =
                     factory().instructions().buildApplyActions()
@@ -290,17 +274,19 @@
 
             OFMessage opticalFlowEntry =
                     factory().buildFlowAdd()
-                                 .setMatch(matchPort)
-                                 .setInstructions(instructions)
-                                 .setXid(getNextTransactionId())
-                                 .build();
+                                .setMatch(matchPort)
+                                .setPriority(100)
+                                .setInstructions(instructions)
+                                .setXid(getNextTransactionId())
+                                .build();
             log.debug("Adding optical flow in sw {}", getStringId());
             List<OFMessage> msglist = new ArrayList<>(1);
             msglist.add(opticalFlowEntry);
             write(msglist);
+            sendBarrier(true);
 
-        } else if (getId() == 0x0000ffffffffff04L) {
-            final int inport = 23;
+        } else if (getId() == 0x0000ffffffffff02L) {
+            final int inport = 21;
             final int outport = 11;
             //Circuit signal id
             CircuitSignalID sigID = getSignalID(lambda);
@@ -318,15 +304,16 @@
             OFMatchV3 matchPort =
                     factory().buildMatchV3()
                                 .setOxmList(OFOxmList.of(fieldPort,
-                                                         fieldSigType,
-                                                         fieldSigIDMatch))
+                                                         fieldSigIDMatch,
+                                                         fieldSigType
+                                ))
                                 .build();
 
             // Set Action outport
             List<OFAction> actionList = new ArrayList<>();
             OFAction actionOutPort =
                     factory().actions().output(OFPort.of(outport),
-                                                  Short.MAX_VALUE);
+                                                  0xffff);
 
             actionList.add(actionOutPort);
 
@@ -339,17 +326,184 @@
 
             OFMessage opticalFlowEntry =
                     factory().buildFlowAdd()
-                                 .setMatch(matchPort)
-                                 .setInstructions(instructions)
-                                 .setXid(getNextTransactionId())
-                                 .build();
+                                .setMatch(matchPort)
+                                .setPriority(100)
+                                .setInstructions(instructions)
+                                .setXid(getNextTransactionId())
+                                .build();
             log.debug("Adding optical flow in sw {}", getStringId());
             List<OFMessage> msglist = new ArrayList<>(1);
             msglist.add(opticalFlowEntry);
             write(msglist);
+            sendBarrier(true);
         }
 
     }
+    private void testReverseMA() throws IOException {
+        log.debug("LINC OE *** Testing MA ");
+        short lambda = 1;
+        if (getId() == 0x0000ffffffffff02L) {
+            final int inport = 11;
+            final int outport = 21;
+            //Circuit signal id
+            CircuitSignalID sigID = getSignalID(lambda);
+
+            OFOxmOchSigidBasic ofOxmOchSigidBasic =
+                    factory().oxms().ochSigidBasic(sigID);
+
+            //Match Port
+            OFOxmInPort fieldPort = factory().oxms()
+                                                .inPort(OFPort.of(inport));
+            OFMatchV3 matchPort =
+                    factory()
+                            .buildMatchV3().
+                            setOxmList(OFOxmList.of(fieldPort)).build();
+
+
+            // Set Action outport ,sigType and sigID
+            List<OFAction> actionList = new ArrayList<>();
+            OFAction actionOutPort =
+                    factory().actions().output(OFPort.of(outport),
+                                                  0xffff);
+
+            OFActionCircuit actionCircuit = factory()
+                    .actions()
+                    .circuit(ofOxmOchSigidBasic);
+            actionList.add(actionCircuit);
+            actionList.add(actionOutPort);
+
+            OFInstruction instructionAction =
+                    factory().instructions().buildApplyActions()
+                                .setActions(actionList)
+                                .build();
+            List<OFInstruction> instructions =
+                    Collections.singletonList(instructionAction);
+
+            OFMessage opticalFlowEntry =
+                    factory().buildFlowAdd()
+                                .setMatch(matchPort)
+                                .setPriority(100)
+                                .setInstructions(instructions)
+                                .setXid(getNextTransactionId())
+                                .build();
+            log.debug("Adding optical flow in sw {}", getStringId());
+            List<OFMessage> msglist = new ArrayList<>(1);
+            msglist.add(opticalFlowEntry);
+            write(msglist);
+            sendBarrier(true);
+        } else if (getId() == 0x0000ffffffffff03L) {
+            final int inport = 31;
+            final int outport = 30;
+            //Circuit signal id
+            CircuitSignalID sigID = getSignalID(lambda);
+
+            OFOxmOchSigid fieldSigIDMatch = factory().oxms().ochSigid(sigID);
+            OFOxmOchSigtype fieldSigType = factory()
+                    .oxms()
+                    .ochSigtype(SIGNAL_TYPE);
+
+            OFOxmOchSigidBasic ofOxmOchSigidBasic =
+                    factory().oxms().ochSigidBasic(sigID);
+
+            //Match Port,SigType,SigID
+            OFOxmInPort fieldPort = factory()
+                    .oxms()
+                    .inPort(OFPort.of(inport));
+            OFMatchV3 matchPort = factory()
+                    .buildMatchV3()
+                    .setOxmList(OFOxmList.of(fieldPort,
+                                             fieldSigIDMatch,
+                                             fieldSigType
+                    ))
+                    .build();
+
+            // Set Action outport ,SigType, sigID
+            List<OFAction> actionList = new ArrayList<>();
+            OFAction actionOutPort =
+                    factory().actions().output(OFPort.of(outport),
+                                                  0xffff);
+            OFActionCircuit actionCircuit = factory()
+                    .actions()
+                    .circuit(ofOxmOchSigidBasic);
+
+            actionList.add(actionCircuit);
+            actionList.add(actionOutPort);
+
+            OFInstruction instructionAction =
+                    factory().instructions().buildApplyActions()
+                                .setActions(actionList)
+                                .build();
+            List<OFInstruction> instructions =
+                    Collections.singletonList(instructionAction);
+
+            OFMessage opticalFlowEntry =
+                    factory().buildFlowAdd()
+                                .setMatch(matchPort)
+                                .setPriority(100)
+                                .setInstructions(instructions)
+                                .setXid(getNextTransactionId())
+                                .build();
+            log.debug("Adding optical flow in sw {}", getStringId());
+            List<OFMessage> msglist = new ArrayList<>(1);
+            msglist.add(opticalFlowEntry);
+            write(msglist);
+            sendBarrier(true);
+
+        } else if (getId() == 0x0000ffffffffff01L) {
+            final int inport = 20;
+            final int outport = 10;
+            //Circuit signal id
+            CircuitSignalID sigID = getSignalID(lambda);
+
+            OFOxmOchSigid fieldSigIDMatch = factory().oxms().ochSigid(sigID);
+            OFOxmOchSigtype fieldSigType = factory()
+                    .oxms()
+                    .ochSigtype(SIGNAL_TYPE);
+
+
+            //Match Port, sig type and sig id
+            OFOxmInPort fieldPort = factory()
+                    .oxms()
+                    .inPort(OFPort.of(inport));
+            OFMatchV3 matchPort =
+                    factory().buildMatchV3()
+                                .setOxmList(OFOxmList.of(fieldPort,
+                                                         fieldSigIDMatch,
+                                                         fieldSigType
+                                ))
+                                .build();
+
+            // Set Action outport
+            List<OFAction> actionList = new ArrayList<>();
+            OFAction actionOutPort =
+                    factory().actions().output(OFPort.of(outport),
+                                                  0xffff);
+
+            actionList.add(actionOutPort);
+
+            OFInstruction instructionAction =
+                    factory().instructions().buildApplyActions()
+                                .setActions(actionList)
+                                .build();
+            List<OFInstruction> instructions =
+                    Collections.singletonList(instructionAction);
+
+            OFMessage opticalFlowEntry =
+                    factory().buildFlowAdd()
+                                .setMatch(matchPort)
+                                .setPriority(100)
+                                .setInstructions(instructions)
+                                .setXid(getNextTransactionId())
+                                .build();
+            log.debug("Adding optical flow in sw {}", getStringId());
+            List<OFMessage> msglist = new ArrayList<>(1);
+            msglist.add(opticalFlowEntry);
+            write(msglist);
+            sendBarrier(true);
+        }
+
+    }
+
 
     // Todo remove - for testing purpose only
     private static CircuitSignalID getSignalID(short lambda) {
@@ -365,9 +519,21 @@
         return signalID;
     }
 
+    private void sendBarrier(boolean finalBarrier) throws IOException {
+        int xid = getNextTransactionId();
+        if (finalBarrier) {
+            barrierXidToWaitFor = xid;
+        }
+        OFBarrierRequest br = factory()
+                .buildBarrierRequest()
+                .setXid(xid)
+                .build();
+        sendMsg(br);
+    }
+
     @Override
     public void write(OFMessage msg) {
-        this.channel.write(msg);
+        this.sendMsg(msg);
     }
 
     @Override
diff --git a/pom.xml b/pom.xml
index 9b275ab..00fc37a 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>
@@ -502,7 +521,7 @@
                         <group>
                             <title>Core Subsystems</title>
                             <packages>
-                                org.onlab.onos.impl:org.onlab.onos.cluster.impl:org.onlab.onos.net.device.impl:org.onlab.onos.net.link.impl:org.onlab.onos.net.host.impl:org.onlab.onos.net.topology.impl:org.onlab.onos.net.packet.impl:org.onlab.onos.net.flow.impl:org.onlab.onos.store.trivial.*:org.onlab.onos.net.*.impl:org.onlab.onos.event.impl:org.onlab.onos.store.*:org.onlab.onos.net.intent.impl:org.onlab.onos.net.proxyarp.impl:org.onlab.onos.mastership.impl
+                                org.onlab.onos.impl:org.onlab.onos.cluster.impl:org.onlab.onos.net.device.impl:org.onlab.onos.net.link.impl:org.onlab.onos.net.host.impl:org.onlab.onos.net.topology.impl:org.onlab.onos.net.packet.impl:org.onlab.onos.net.flow.impl:org.onlab.onos.store.trivial.*:org.onlab.onos.net.*.impl:org.onlab.onos.event.impl:org.onlab.onos.store.*:org.onlab.onos.net.intent.impl:org.onlab.onos.net.proxyarp.impl:org.onlab.onos.mastership.impl:org.onlab.onos.json:org.onlab.onos.json.*
                             </packages>
                         </group>
                         <group>
@@ -527,10 +546,11 @@
                         <group>
                             <title>Sample Applications</title>
                             <packages>
-                                org.onlab.onos.tvue:org.onlab.onos.fwd:org.onlab.onos.ifwd:org.onlab.onos.mobility:org.onlab.onos.proxyarp:org.onlab.onos.foo
+                                org.onlab.onos.tvue:org.onlab.onos.fwd:org.onlab.onos.ifwd:org.onlab.onos.mobility:org.onlab.onos.proxyarp:org.onlab.onos.foo:org.onlab.onos.calendar
                             </packages>
                         </group>
                     </groups>
+                    <excludePackageNames>org.onlab.thirdparty</excludePackageNames>
                 </configuration>
             </plugin>
 
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..3319302 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
@@ -66,6 +66,7 @@
  * TODO: add 'fast discovery' mode: drop LLDPs in destination switch but listen
  * for flow_removed messages
  */
+@Deprecated
 public class LinkDiscovery implements TimerTask {
 
     private final OpenFlowSwitch sw;
@@ -339,9 +340,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/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/OpenFlowLinkProvider.java b/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/OpenFlowLinkProvider.java
index f1e3861..7f16eaa 100644
--- a/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/OpenFlowLinkProvider.java
+++ b/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/OpenFlowLinkProvider.java
@@ -35,6 +35,7 @@
  * infrastructure links.
  */
 @Component(immediate = true)
+@Deprecated
 public class OpenFlowLinkProvider extends AbstractProvider implements LinkProvider {
 
     private final Logger log = getLogger(getClass());
diff --git a/tools/build/envDefaults b/tools/build/envDefaults
index cbc6577..9c17212 100644
--- a/tools/build/envDefaults
+++ b/tools/build/envDefaults
@@ -9,6 +9,10 @@
 export KARAF_TAR=${KARAF_TAR:-~/Downloads/apache-karaf-3.0.1.tar.gz}
 export KARAF_DIST=$(basename $KARAF_ZIP .zip)
 
+# Add ONOS-specific directories to the exectable PATH
+export PATH="$PATH:$ONOS_ROOT/tools/dev/bin:$ONOS_ROOT/tools/test/bin"
+export PATH="$PATH:$ONOS_ROOT/tools/build"
+
 # Fallback build number us derived from from the user name & time
 export BUILD_NUMBER=${BUILD_NUMBER:-$(id -un)~$(date +'%Y/%m/%d@%H:%M')}
 
@@ -21,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 6b66ff1..f9754af 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'
@@ -61,10 +66,10 @@
     if [ -n "$1" ]; then
         [ ! -f $ONOS_ROOT/tools/test/cells/$1 ] && \
             echo "No such cell: $1" >&2 && return 1
-        unset OC1 OC2 OC3 OC4 OC5 OC6 OC7 OC8 OC9 OCN OCI
-        . $ONOS_ROOT/tools/test/cells/$1
-        export OCI=$OC1
+        unset ONOS_CELL ONOS_NIC ONOS_FEATURES
+        unset OC0 OC1 OC2 OC3 OC4 OC5 OC6 OC7 OC8 OC9 OCN OCI
         export ONOS_CELL=$1
+        . $ONOS_ROOT/tools/test/cells/$1
         cell
     else
         env | egrep "ONOS_CELL"
diff --git a/tools/dev/onos.cshrc b/tools/dev/onos.cshrc
new file mode 100644
index 0000000..8dba4b2
--- /dev/null
+++ b/tools/dev/onos.cshrc
@@ -0,0 +1,36 @@
+#!/bin/tcsh
+# ONOS developer csh/tcsh profile conveniences
+# Simply include in your own $HOME/.cshrc file. E.g.:
+#
+#    setenv ONOS_ROOT $HOME/onos
+#    if ( -f $ONOS_ROOT/tools/dev/onos.cshrc ) then
+#        source $ONOS_ROOT/tools/dev/onos.cshrc
+#    endif
+#     
+
+# Root of the ONOS source tree
+if ( ! $?ONOS_ROOT ) then
+    setenv ONOS_ROOT $HOME/onos
+endif
+
+# Setup some environmental context for developers
+if ( ! $?JAVA_HOME ) then
+    if ( -x /usr/libexec/java_home ) then
+        setenv JAVA_HOME `/usr/libexec/java_home -v 1.7`
+    else if ( -d /usr/lib/jvm/java-7-openjdk-amd64 ) then
+        setenv JAVA_HOME /usr/lib/jvm/java-7-openjdk-amd64
+    endif
+endif
+if ( ! $?MAVEN ) then
+    setenv MAVEN $HOME/Applications/apache-maven-3.2.2
+endif
+if ( ! $?KARAF ) then
+    setenv KARAF $HOME/Applications/apache-karaf-3.0.1
+endif
+setenv KARAF_LOG $KARAF/data/log/karaf.log
+
+alias onos-setup-cell ' ( $ONOS_ROOT/tools/test/bin/onos-show-cell \!^ ) && setenv ONOS_CELL \!^'
+
+set path=( $path $ONOS_ROOT/tools/dev/bin $ONOS_ROOT/tools/test/bin )
+set path=( $path $ONOS_ROOT/tools/build )
+set path=( $path $KARAF/bin )
diff --git a/tools/test/bin/onos-list-cells b/tools/test/bin/onos-list-cells
new file mode 100755
index 0000000..39a70ee
--- /dev/null
+++ b/tools/test/bin/onos-list-cells
@@ -0,0 +1,18 @@
+#!/bin/bash
+# -----------------------------------------------------------------------------
+# List available ONOS cells configuration.
+# -----------------------------------------------------------------------------
+
+[ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
+. $ONOS_ROOT/tools/build/envDefaults
+
+# Lists available cells
+for cell in $(ls -1 $ONOS_ROOT/tools/test/cells); do
+    if [ ${cell} = "${ONOS_CELL}" ]; then
+        cell_id="${cell} *"
+    else
+        cell_id="${cell}"
+    fi
+    cell_descr="$(grep '^#' $ONOS_ROOT/tools/test/cells/$cell | head -n 1)"
+    printf "%-12s  %s\n" "${cell_id}" "${cell_descr}"
+done
diff --git a/tools/test/bin/onos-service b/tools/test/bin/onos-service
index 1c62ae8..d5fabe5 100755
--- a/tools/test/bin/onos-service
+++ b/tools/test/bin/onos-service
@@ -6,4 +6,39 @@
 [ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
 . $ONOS_ROOT/tools/build/envDefaults
 
-ssh $ONOS_USER@${1:-$OCI} "sudo service onos ${2:-status}"
+function print_usage {
+    command_name=`basename $0`
+    echo "Remotely administer the ONOS service on a single node or the current ONOS cell."
+    echo
+    echo "Usage:     $command_name <TARGET> [COMMAND]"
+    echo "           $command_name [-h | --help]"
+    echo "Options:"
+    echo "    TARGET          The target of the command"
+    echo "    COMMAND         The command to execute. Default value is 'status'"
+    echo "    [-h | --help]   Print this help"
+    echo ""
+    echo "TARGET:  <hostname | --cell>"
+    echo "      hostname        Execute on the specified host name"
+    echo "        --cell        Execute on the current ONOS cell"
+    echo ""
+    echo "COMMAND: [start|stop|restart|status]"
+    echo ""
+}
+
+# Print usage
+if [ "${1}" = "-h" -o "${1}" = "--help" ]; then
+    print_usage
+    exit 0
+fi
+
+# Select the target
+if [ "${1}" = "--cell" ]; then
+    nodes=$(env | sort | egrep "OC[0-9]+" | cut -d= -f2)
+else
+    nodes=${1:-$OCI}
+fi
+
+# Execute the remote commands
+for node in $nodes; do
+    ssh $ONOS_USER@${node} "sudo service onos ${2:-status}"
+done
diff --git a/tools/test/bin/onos-show-cell b/tools/test/bin/onos-show-cell
new file mode 100755
index 0000000..5aee338
--- /dev/null
+++ b/tools/test/bin/onos-show-cell
@@ -0,0 +1,53 @@
+#!/bin/bash
+# -----------------------------------------------------------------------------
+# Print the configuration of an ONOS cell.
+# -----------------------------------------------------------------------------
+
+[ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
+. $ONOS_ROOT/tools/build/envDefaults
+
+function print_usage {
+    echo "Print the configuration of an ONOS cell."
+    echo "If no arguments are specified, it will print the configuration for the default"
+    echo "ONOS cell as specified in the 'ONOS_CELL' environmental variable."
+    echo
+    echo "Optional arguments:"
+    echo "  [cell-name]       Print the configuration of 'cell-name'"
+    echo "  [-h | --help]     Print this help"
+}
+
+if [ "${1}" = "-h" -o "${1}" = "--help" ]; then
+    print_usage
+    exit 0
+fi
+
+if [ -n "${1}" ]; then
+    cell="${1}"
+else
+    if [ -z "${ONOS_CELL}" ]; then
+        echo "Environmental variable 'ONOS_CELL' is not defiled"
+        exit 1
+    else
+        cell="${ONOS_CELL}"
+    fi
+fi
+
+if [ ! -f $ONOS_ROOT/tools/test/cells/${cell} ]; then
+    echo "No such cell: ${cell}"
+    exit 1
+fi
+
+# Load the cell setup
+. $ONOS_ROOT/tools/test/cells/${cell}
+
+echo "ONOS_CELL=${ONOS_CELL}"
+echo "ONOS_NIC=${ONOS_NIC}"
+for n in {0..9}; do
+    ocn="OC${n}"
+    if [ -n "${!ocn}" ]; then
+        echo "$ocn=${!ocn}"
+    fi
+done
+echo "OCN=${OCN}"
+echo "OCI=${OCI}"
+echo "ONOS_FEATURES=${ONOS_FEATURES}"
diff --git a/tools/test/cells/cbench b/tools/test/cells/cbench
index 692bb53..2e0b478 100644
--- a/tools/test/cells/cbench
+++ b/tools/test/cells/cbench
@@ -1,7 +1,10 @@
 # Local VirtualBox-based single ONOS instance & ONOS mininet box
 
+export ONOS_CELL="cbench"
+
 export ONOS_NIC=192.168.56.*
 export OC1="192.168.56.103"
 export OCN="192.168.56.103"
+export OCI="${OC1}"
 
 export ONOS_FEATURES="webconsole,onos-api,onos-core-trivial,onos-cli,onos-openflow,onos-app-fwd"
diff --git a/tools/test/cells/local b/tools/test/cells/local
index 94fa24e..f506c08 100644
--- a/tools/test/cells/local
+++ b/tools/test/cells/local
@@ -1,7 +1,11 @@
 # Local VirtualBox-based ONOS instances 1,2 & ONOS mininet box
 
+export ONOS_CELL="local"
+
 export ONOS_NIC=192.168.56.*
 export OC1="192.168.56.101"
 export OC2="192.168.56.102"
-
 export OCN="192.168.56.103"
+export OCI="${OC1}"
+
+export ONOS_FEATURES="${ONOS_FEATURES:-webconsole,onos-api,onos-core,onos-cli,onos-openflow,onos-app-fwd,onos-app-proxyarp,onos-app-tvue}"
diff --git a/tools/test/cells/office b/tools/test/cells/office
index e7aa1ba..7c6345b 100644
--- a/tools/test/cells/office
+++ b/tools/test/cells/office
@@ -1,6 +1,9 @@
 # ProxMox-based cell of ONOS instance; no mininet-box
 
-export ONOS_FEATURES="webconsole,onos-api,onos-core-trivial,onos-cli,onos-openflow,onos-app-fwd,onos-app-mobility,onos-app-tvue,onos-app-proxyarp"
+export ONOS_CELL="office"
 
 export ONOS_NIC="10.1.10.*"
 export OC1="10.1.10.223"
+export OCI="${OC1}"
+
+export ONOS_FEATURES="webconsole,onos-api,onos-core-trivial,onos-cli,onos-openflow,onos-app-fwd,onos-app-mobility,onos-app-tvue,onos-app-proxyarp"
diff --git a/tools/test/cells/prox b/tools/test/cells/prox
index 1731eb8..557388f 100644
--- a/tools/test/cells/prox
+++ b/tools/test/cells/prox
@@ -1,7 +1,11 @@
 # ProxMox-based cell of ONOS instances 1,2 & ONOS mininet box
 
+export ONOS_CELL="prox"
+
 export ONOS_NIC="10.1.9.*"
 export OC1="10.1.9.94"
 export OC2="10.1.9.82"
-
 export OCN="10.1.9.93"
+export OCI="${OC1}"
+
+export ONOS_FEATURES=""
diff --git a/tools/test/cells/single b/tools/test/cells/single
index 6dcd4d5..6b13756 100644
--- a/tools/test/cells/single
+++ b/tools/test/cells/single
@@ -1,5 +1,10 @@
 # Local VirtualBox-based single ONOS instance & ONOS mininet box
 
+export ONOS_CELL="single"
+
 export ONOS_NIC=192.168.56.*
 export OC1="192.168.56.101"
 export OCN="192.168.56.103"
+export OCI="${OC1}"
+
+export ONOS_FEATURES="${ONOS_FEATURES:-webconsole,onos-api,onos-core-trivial,onos-cli,onos-openflow,onos-app-fwd,onos-app-proxyarp,onos-app-tvue}"
diff --git a/tools/test/cells/triple b/tools/test/cells/triple
index fa4f052..104eb05 100644
--- a/tools/test/cells/triple
+++ b/tools/test/cells/triple
@@ -1,8 +1,12 @@
 # Local VirtualBox-based ONOS instances 1,2,3 & ONOS mininet box
 
+export ONOS_CELL="triple"
+
 export ONOS_NIC=192.168.56.*
 export OC1="192.168.56.101"
 export OC2="192.168.56.102"
 export OC3="192.168.56.104"
-
 export OCN="192.168.56.103"
+export OCI="${OC1}"
+
+export ONOS_FEATURES=""
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/misc/src/main/java/org/onlab/util/TestUtils.java b/utils/misc/src/main/java/org/onlab/util/TestUtils.java
new file mode 100644
index 0000000..7e59564
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/util/TestUtils.java
@@ -0,0 +1,169 @@
+package org.onlab.util;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+
+/**
+ * Utilities for testing.
+ */
+public final class TestUtils {
+
+    /**
+     * Sets the field, bypassing scope restriction.
+     *
+     * @param subject Object where the field belongs
+     * @param fieldName name of the field to set
+     * @param value value to set to the field.
+     * @param <T> subject type
+     * @param <U> value type
+     * @throws TestUtilsException if there are reflection errors while setting
+     * the field
+     */
+    public static <T, U> void setField(T subject, String fieldName, U value)
+            throws TestUtilsException {
+        @SuppressWarnings("unchecked")
+        Class<T> clazz = (Class<T>) subject.getClass();
+        try {
+            Field field = clazz.getDeclaredField(fieldName);
+            field.setAccessible(true);
+            field.set(subject, value);
+        } catch (NoSuchFieldException | SecurityException |
+                 IllegalArgumentException | IllegalAccessException e) {
+            throw new TestUtilsException("setField failed", e);
+        }
+    }
+
+    /**
+     * Gets the field, bypassing scope restriction.
+     *
+     * @param subject Object where the field belongs
+     * @param fieldName name of the field to get
+     * @return value of the field.
+     * @param <T> subject type
+     * @param <U> field value type
+     * @throws TestUtilsException if there are reflection errors while getting
+     * the field
+     */
+    public static <T, U> U getField(T subject, String fieldName)
+            throws TestUtilsException {
+        try {
+            @SuppressWarnings("unchecked")
+            Class<T> clazz = (Class<T>) subject.getClass();
+            Field field = clazz.getDeclaredField(fieldName);
+            field.setAccessible(true);
+
+            @SuppressWarnings("unchecked")
+            U result = (U) field.get(subject);
+            return result;
+        } catch (NoSuchFieldException | SecurityException |
+                 IllegalArgumentException | IllegalAccessException e) {
+            throw new TestUtilsException("getField failed", e);
+        }
+    }
+
+    /**
+     * Calls the method, bypassing scope restriction.
+     *
+     * @param subject Object where the method belongs
+     * @param methodName name of the method to call
+     * @param paramTypes formal parameter type array
+     * @param args arguments
+     * @return return value or null if void
+     * @param <T> subject type
+     * @param <U> return value type
+     * @throws TestUtilsException if there are reflection errors while calling
+     * the method
+     */
+    public static <T, U> U callMethod(T subject, String methodName,
+            Class<?>[] paramTypes, Object...args) throws TestUtilsException {
+
+        try {
+            @SuppressWarnings("unchecked")
+            Class<T> clazz = (Class<T>) subject.getClass();
+            final Method method;
+            if (paramTypes == null || paramTypes.length == 0) {
+                method = clazz.getDeclaredMethod(methodName);
+            } else {
+                method = clazz.getDeclaredMethod(methodName, paramTypes);
+            }
+            method.setAccessible(true);
+
+            @SuppressWarnings("unchecked")
+            U result = (U) method.invoke(subject, args);
+            return result;
+        } catch (NoSuchMethodException | SecurityException |
+                IllegalAccessException | IllegalArgumentException |
+                InvocationTargetException e) {
+            throw new TestUtilsException("callMethod failed", e);
+        }
+    }
+
+    /**
+     * Calls the method, bypassing scope restriction.
+     *
+     * @param subject Object where the method belongs
+     * @param methodName name of the method to call
+     * @param paramType formal parameter type
+     * @param arg argument
+     * @return return value or null if void
+     * @param <T> subject type
+     * @param <U> return value type
+     * @throws TestUtilsException if there are reflection errors while calling
+     * the method
+     */
+    public static <T, U> U callMethod(T subject, String methodName,
+            Class<?> paramType, Object arg) throws TestUtilsException {
+        return callMethod(subject, methodName, new Class<?>[]{paramType}, arg);
+    }
+
+    /**
+     * Triggers an allocation of an object of type <T> and forces a call to
+     * the private constructor.
+     *
+     * @param constructor Constructor to call
+     * @param <T> type of the object to create
+     * @return created object of type <T>
+     * @throws TestUtilsException if there are reflection errors while calling
+     * the constructor
+     */
+    public static <T> T callConstructor(Constructor<T> constructor)
+            throws TestUtilsException {
+        try {
+            constructor.setAccessible(true);
+            return constructor.newInstance();
+        } catch (InstantiationException | IllegalAccessException |
+                InvocationTargetException error) {
+            throw new TestUtilsException("callConstructor failed", error);
+        }
+    }
+
+    /**
+     * Avoid instantiation.
+     */
+    private TestUtils() {}
+
+    /**
+     * Exception that can be thrown if problems are encountered while executing
+     * the utility method. These are usually problems accessing fields/methods
+     * through reflection. The original exception can be found by examining the
+     * cause.
+     */
+    public static class TestUtilsException extends Exception {
+
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * Constructs a new exception with the specified detail message and
+         * cause.
+         *
+         * @param message the detail message
+         * @param cause the original cause of this exception
+         */
+        public TestUtilsException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+}
diff --git a/utils/misc/src/test/java/org/onlab/util/TestUtilsTest.java b/utils/misc/src/test/java/org/onlab/util/TestUtilsTest.java
new file mode 100644
index 0000000..58e60c1
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/util/TestUtilsTest.java
@@ -0,0 +1,170 @@
+package org.onlab.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.util.TestUtils.TestUtilsException;
+
+/**
+ * Test and usage examples for TestUtils.
+ */
+public class TestUtilsTest {
+
+    /**
+     * Test data.
+     */
+    private static final class TestClass {
+
+        @SuppressWarnings("unused")
+        private int privateField = 42;
+
+        @SuppressWarnings("unused")
+        protected int protectedField = 2501; // CHECKSTYLE IGNORE THIS LINE
+
+        /**
+         * Protected method with multiple argument.
+         *
+         * @param x simply returns
+         * @param y not used
+         * @return x
+         */
+        @SuppressWarnings("unused")
+        private int privateMethod(Number x, Long y) {
+            return x.intValue();
+        }
+
+        /**
+         * Protected method with no argument.
+         *
+         * @return int
+         */
+        @SuppressWarnings("unused")
+        protected int protectedMethod() {
+            return 42;
+        }
+
+        /**
+         * Method returning array.
+         *
+         * @param ary random array
+         * @return ary
+         */
+        @SuppressWarnings("unused")
+        private int[] arrayReturnMethod(int[] ary) {
+            return ary;
+        }
+
+        /**
+         * Method without return value.
+         *
+         * @param s ignored
+         */
+        @SuppressWarnings("unused")
+        private void voidMethod(String s) {
+            System.out.println(s);
+        }
+    }
+
+    private TestClass test;
+
+    /**
+     * Sets up the test fixture.
+     */
+    @Before
+    public void setUp() {
+        test = new TestClass();
+    }
+
+    /**
+     * Example to access private field.
+     *
+     * @throws TestUtilsException TestUtils error
+     */
+    @Test
+    public void testSetGetPrivateField() throws TestUtilsException {
+
+        assertEquals(42, TestUtils.getField(test, "privateField"));
+        TestUtils.setField(test, "privateField", 0xDEAD);
+        assertEquals(0xDEAD, TestUtils.getField(test, "privateField"));
+    }
+
+    /**
+     * Example to access protected field.
+     *
+     * @throws TestUtilsException TestUtils error
+     */
+    @Test
+    public void testSetGetProtectedField() throws TestUtilsException {
+
+        assertEquals(2501, TestUtils.getField(test, "protectedField"));
+        TestUtils.setField(test, "protectedField", 0xBEEF);
+        assertEquals(0xBEEF, TestUtils.getField(test, "protectedField"));
+    }
+
+    /**
+     * Example to call private method and multiple parameters.
+     * <p/>
+     * It also illustrates that paramTypes must match declared type,
+     * not the runtime types of arguments.
+     *
+     * @throws TestUtilsException TestUtils error
+     */
+    @Test
+    public void testCallPrivateMethod() throws TestUtilsException {
+
+        int result = TestUtils.callMethod(test, "privateMethod",
+                        new Class<?>[] {Number.class, Long.class},
+                        Long.valueOf(42), Long.valueOf(32));
+        assertEquals(42, result);
+    }
+
+    /**
+     * Example to call protected method and no parameters.
+     *
+     * @throws TestUtilsException TestUtils error
+     */
+    @Test
+    public void testCallProtectedMethod() throws TestUtilsException {
+
+        int result = TestUtils.callMethod(test, "protectedMethod",
+                        new Class<?>[] {});
+        assertEquals(42, result);
+    }
+
+    /**
+     * Example to call method returning array.
+     * <p/>
+     * Note: It is not required to receive as Object.
+     * Following is just verifying it is not Boxed arrays.
+     *
+     * @throws TestUtilsException TestUtils error
+     */
+    @Test
+    public void testCallArrayReturnMethod() throws TestUtilsException {
+
+        int[] array = {1, 2, 3};
+        Object aryResult = TestUtils.callMethod(test, "arrayReturnMethod",
+                new Class<?>[] {int[].class}, array);
+        assertEquals(int[].class, aryResult.getClass());
+        assertArrayEquals(array, (int[]) aryResult);
+    }
+
+
+    /**
+     * Example to call void returning method.
+     * <p/>
+     * Note: Return value will be null for void methods.
+     *
+     * @throws TestUtilsException TestUtils error
+     */
+    @Test
+    public void testCallVoidReturnMethod() throws TestUtilsException {
+
+        Object voidResult = TestUtils.callMethod(test, "voidMethod",
+                String.class, "foobar");
+        assertNull(voidResult);
+    }
+}
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 {
+
+}
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index f959f93..d68a706 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -2,6 +2,9 @@
 <html>
 <head>
     <title>ONOS GUI</title>
+
+    <script src="libs/d3.min.js"></script>
+    <script src="libs/jquery-2.1.1.min.js"></script>
 </head>
 <body>
     <h1>ONOS GUI</h1>