Merge branch 'master' of ssh://gerrit.onlab.us:29418/onos-next
diff --git a/cli/src/main/java/org/onlab/onos/cli/NodesListCommand.java b/cli/src/main/java/org/onlab/onos/cli/NodesListCommand.java
new file mode 100644
index 0000000..1057d6e
--- /dev/null
+++ b/cli/src/main/java/org/onlab/onos/cli/NodesListCommand.java
@@ -0,0 +1,45 @@
+package org.onlab.onos.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.onos.cluster.ClusterService;
+import org.onlab.onos.cluster.ControllerNode;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+/**
+ * Lists all controller cluster nodes.
+ */
+@Command(scope = "onos", name = "nodes",
+         description = "Lists all controller cluster nodes")
+public class NodesListCommand extends AbstractShellCommand {
+
+    private static final String FMT =
+            "id=%s, ip=%s, state=%s %s";
+
+    protected static final Comparator<ControllerNode> ID_COMPARATOR =
+            new Comparator<ControllerNode>() {
+        @Override
+        public int compare(ControllerNode ci1, ControllerNode ci2) {
+            return ci1.id().toString().compareTo(ci2.id().toString());
+        }
+    };
+
+    @Override
+    protected Object doExecute() throws Exception {
+        ClusterService service = getService(ClusterService.class);
+        List<ControllerNode> nodes = newArrayList(service.getNodes());
+        Collections.sort(nodes, ID_COMPARATOR);
+        ControllerNode self = service.getLocalNode();
+        for (ControllerNode node : nodes) {
+            print(FMT, node.id(), node.ip(),
+                  service.getState(node.id()),
+                  node.equals(self) ? "*" : "");
+        }
+        return null;
+    }
+
+}
diff --git a/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
index 9d8259e..419ed16 100644
--- a/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
+++ b/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -2,6 +2,9 @@
 
     <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
         <command>
+            <action class="org.onlab.onos.cli.NodesListCommand"/>
+        </command>
+        <command>
             <action class="org.onlab.onos.cli.net.FlowsListCommand"/>
         </command>
         <command>
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/ClusterEvent.java b/core/api/src/main/java/org/onlab/onos/cluster/ClusterEvent.java
index 6737f68..a47c43f 100644
--- a/core/api/src/main/java/org/onlab/onos/cluster/ClusterEvent.java
+++ b/core/api/src/main/java/org/onlab/onos/cluster/ClusterEvent.java
@@ -5,10 +5,10 @@
 /**
  * Describes cluster-related event.
  */
-public class ClusterEvent extends AbstractEvent<ClusterEvent.Type, ControllerInstance> {
+public class ClusterEvent extends AbstractEvent<ClusterEvent.Type, ControllerNode> {
 
     /**
-     * Type of device events.
+     * Type of cluster-related events.
      */
     public enum Type {
         /**
@@ -24,14 +24,13 @@
         /**
          * Signifies that a cluster instance became active.
          */
-        INSTANCE_ACTIVE,
+        INSTANCE_ACTIVATED,
 
         /**
          * Signifies that a cluster instance became inactive.
          */
-        INSTANCE_INACTIVE
+        INSTANCE_DEACTIVATED
     }
-    // TODO: do we need to fix the verv/adjective mix? discuss
 
     /**
      * Creates an event of a given type and for the specified instance and the
@@ -40,7 +39,7 @@
      * @param type     cluster event type
      * @param instance cluster device subject
      */
-    public ClusterEvent(Type type, ControllerInstance instance) {
+    public ClusterEvent(Type type, ControllerNode instance) {
         super(type, instance);
     }
 
@@ -51,7 +50,7 @@
      * @param instance event device subject
      * @param time     occurrence time
      */
-    public ClusterEvent(Type type, ControllerInstance instance, long time) {
+    public ClusterEvent(Type type, ControllerNode instance, long time) {
         super(type, instance, time);
     }
 
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/ClusterEventListener.java b/core/api/src/main/java/org/onlab/onos/cluster/ClusterEventListener.java
new file mode 100644
index 0000000..5cc4ea0
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/cluster/ClusterEventListener.java
@@ -0,0 +1,9 @@
+package org.onlab.onos.cluster;
+
+import org.onlab.onos.event.EventListener;
+
+/**
+ * Entity capable of receiving device cluster-related events.
+ */
+public interface ClusterEventListener  extends EventListener<ClusterEvent> {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/ClusterService.java b/core/api/src/main/java/org/onlab/onos/cluster/ClusterService.java
index afbf0d5..9c0af8f 100644
--- a/core/api/src/main/java/org/onlab/onos/cluster/ClusterService.java
+++ b/core/api/src/main/java/org/onlab/onos/cluster/ClusterService.java
@@ -3,27 +3,53 @@
 import java.util.Set;
 
 /**
- * Service for obtaining information about the individual instances within
+ * Service for obtaining information about the individual nodes within
  * the controller cluster.
  */
 public interface ClusterService {
 
     /**
+     * Returns the local controller node.
+     *
+     * @return local controller node
+     */
+    ControllerNode getLocalNode();
+
+    /**
      * Returns the set of current cluster members.
      *
      * @return set of cluster members
      */
-    Set<ControllerInstance> getInstances();
+    Set<ControllerNode> getNodes();
 
     /**
-     * Returns the availability state of the specified controller instance.
+     * Returns the specified controller node.
      *
+     * @param nodeId controller node identifier
+     * @return controller node
+     */
+    ControllerNode getNode(NodeId nodeId);
+
+    /**
+     * Returns the availability state of the specified controller node.
+     *
+     * @param nodeId controller node identifier
      * @return availability state
      */
-    ControllerInstance.State getState(ControllerInstance instance);
-    // TODO: determine if this would be better attached to ControllerInstance directly
+    ControllerNode.State getState(NodeId nodeId);
 
+    /**
+     * Adds the specified cluster event listener.
+     *
+     * @param listener the cluster listener
+     */
+    void addListener(ClusterEventListener listener);
 
-    // addListener, removeListener
+    /**
+     * Removes the specified cluster event listener.
+     *
+     * @param listener the cluster listener
+     */
+    void removeListener(ClusterEventListener listener);
 
 }
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/ClusterStore.java b/core/api/src/main/java/org/onlab/onos/cluster/ClusterStore.java
new file mode 100644
index 0000000..7d4b71f
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/cluster/ClusterStore.java
@@ -0,0 +1,40 @@
+package org.onlab.onos.cluster;
+
+import java.util.Set;
+
+/**
+ * Manages inventory of controller cluster nodes; not intended for direct use.
+ */
+public interface ClusterStore {
+
+    /**
+     * Returns the local controller instance.
+     *
+     * @return local controller instance
+     */
+    ControllerNode getLocalNode();
+
+    /**
+     * Returns the set of current cluster members.
+     *
+     * @return set of cluster members
+     */
+    Set<ControllerNode> getNodes();
+
+    /**
+     * Returns the specified controller instance.
+     *
+     * @param nodeId controller instance identifier
+     * @return controller instance
+     */
+    ControllerNode getNode(NodeId nodeId);
+
+    /**
+     * Returns the availability state of the specified controller instance.
+     *
+     * @param nodeId controller instance identifier
+     * @return availability state
+     */
+    ControllerNode.State getState(NodeId nodeId);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/ControllerInstance.java b/core/api/src/main/java/org/onlab/onos/cluster/ControllerNode.java
similarity index 92%
rename from core/api/src/main/java/org/onlab/onos/cluster/ControllerInstance.java
rename to core/api/src/main/java/org/onlab/onos/cluster/ControllerNode.java
index 9255175..c6f0cb3 100644
--- a/core/api/src/main/java/org/onlab/onos/cluster/ControllerInstance.java
+++ b/core/api/src/main/java/org/onlab/onos/cluster/ControllerNode.java
@@ -5,7 +5,7 @@
 /**
  * Represents a controller instance as a member in a cluster.
  */
-public interface ControllerInstance {
+public interface ControllerNode {
 
     /** Represents the operational state of the instance. */
     public enum State {
@@ -26,7 +26,7 @@
      *
      * @return instance identifier
      */
-    InstanceId id();
+    NodeId id();
 
     /**
      * Returns the IP address of the controller instance.
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/DefaultControllerNode.java b/core/api/src/main/java/org/onlab/onos/cluster/DefaultControllerNode.java
new file mode 100644
index 0000000..86ea14c
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/cluster/DefaultControllerNode.java
@@ -0,0 +1,66 @@
+package org.onlab.onos.cluster;
+
+import org.onlab.packet.IpPrefix;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Default implementation of a controller instance descriptor.
+ */
+public class DefaultControllerNode implements ControllerNode {
+
+    private final NodeId id;
+    private final IpPrefix ip;
+
+    // For serialization
+    private DefaultControllerNode() {
+        this.id = null;
+        this.ip = null;
+    }
+
+    /**
+     * Creates a new instance with the specified id and IP address.
+     *
+     * @param id instance identifier
+     * @param ip instance IP address
+     */
+    public DefaultControllerNode(NodeId id, IpPrefix ip) {
+        this.id = id;
+        this.ip = ip;
+    }
+
+    @Override
+    public NodeId id() {
+        return id;
+    }
+
+    @Override
+    public IpPrefix ip() {
+        return ip;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o instanceof DefaultControllerNode) {
+            DefaultControllerNode that = (DefaultControllerNode) o;
+            return Objects.equals(this.id, that.id);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this).add("id", id).add("ip", ip).toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/InstanceId.java b/core/api/src/main/java/org/onlab/onos/cluster/InstanceId.java
deleted file mode 100644
index 7292a85..0000000
--- a/core/api/src/main/java/org/onlab/onos/cluster/InstanceId.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.onlab.onos.cluster;
-
-/**
- * Controller cluster identity.
- */
-public interface InstanceId {
-}
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/MastershipAdminService.java b/core/api/src/main/java/org/onlab/onos/cluster/MastershipAdminService.java
index 6c58020..907b3f8 100644
--- a/core/api/src/main/java/org/onlab/onos/cluster/MastershipAdminService.java
+++ b/core/api/src/main/java/org/onlab/onos/cluster/MastershipAdminService.java
@@ -15,6 +15,6 @@
      * @param deviceId device identifier
      * @param role     requested role
      */
-    void setRole(InstanceId instance, DeviceId deviceId, MastershipRole role);
+    void setRole(NodeId instance, DeviceId deviceId, MastershipRole role);
 
 }
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/MastershipEvent.java b/core/api/src/main/java/org/onlab/onos/cluster/MastershipEvent.java
index a835449..2a5e62e 100644
--- a/core/api/src/main/java/org/onlab/onos/cluster/MastershipEvent.java
+++ b/core/api/src/main/java/org/onlab/onos/cluster/MastershipEvent.java
@@ -8,7 +8,7 @@
  */
 public class MastershipEvent extends AbstractEvent<MastershipEvent.Type, DeviceId> {
 
-    InstanceId master;
+    NodeId master;
 
     /**
      * Type of mastership events.
@@ -28,7 +28,7 @@
      * @param device event device subject
      * @param master master ID subject
      */
-    protected MastershipEvent(Type type, DeviceId device, InstanceId master) {
+    protected MastershipEvent(Type type, DeviceId device, NodeId master) {
         super(type, device);
         this.master = master;
     }
@@ -42,7 +42,7 @@
      * @param master master ID subject
      * @param time   occurrence time
      */
-    protected MastershipEvent(Type type, DeviceId device, InstanceId master, long time) {
+    protected MastershipEvent(Type type, DeviceId device, NodeId master, long time) {
         super(type, device, time);
         this.master = master;
     }
@@ -52,7 +52,7 @@
      *
      * @return master ID subject
      */
-    public InstanceId master() {
+    public NodeId master() {
         return master;
     }
 }
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/MastershipListener.java b/core/api/src/main/java/org/onlab/onos/cluster/MastershipListener.java
index 8a49c31..71d65be 100644
--- a/core/api/src/main/java/org/onlab/onos/cluster/MastershipListener.java
+++ b/core/api/src/main/java/org/onlab/onos/cluster/MastershipListener.java
@@ -6,5 +6,4 @@
  * Entity capable of receiving device mastership-related events.
  */
 public interface MastershipListener extends EventListener<MastershipEvent> {
-
 }
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/MastershipService.java b/core/api/src/main/java/org/onlab/onos/cluster/MastershipService.java
index 3592aeb..6a9b60e 100644
--- a/core/api/src/main/java/org/onlab/onos/cluster/MastershipService.java
+++ b/core/api/src/main/java/org/onlab/onos/cluster/MastershipService.java
@@ -19,15 +19,15 @@
      * @param deviceId the identifier of the device
      * @return the ID of the master controller for the device
      */
-    InstanceId getMasterFor(DeviceId deviceId);
+    NodeId getMasterFor(DeviceId deviceId);
 
     /**
      * Returns the devices for which a controller is master.
      *
-     * @param instanceId the ID of the controller
+     * @param nodeId the ID of the controller
      * @return a set of device IDs
      */
-    Set<DeviceId> getDevicesOf(InstanceId instanceId);
+    Set<DeviceId> getDevicesOf(NodeId nodeId);
 
     /**
      * Returns the mastership status of this controller for a given device.
@@ -38,14 +38,14 @@
     MastershipRole requestRoleFor(DeviceId deviceId);
 
     /**
-     * Adds the specified mastership listener.
+     * Adds the specified mastership change listener.
      *
      * @param listener the mastership listener
      */
     void addListener(MastershipListener listener);
 
     /**
-     * Removes the specified device listener.
+     * Removes the specified mastership change listener.
      *
      * @param listener the mastership listener
      */
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/MastershipStore.java b/core/api/src/main/java/org/onlab/onos/cluster/MastershipStore.java
index dad4a75..728e77d 100644
--- a/core/api/src/main/java/org/onlab/onos/cluster/MastershipStore.java
+++ b/core/api/src/main/java/org/onlab/onos/cluster/MastershipStore.java
@@ -6,11 +6,12 @@
 import org.onlab.onos.net.MastershipRole;
 
 /**
- * Manages inventory of mastership roles for devices, across controller instances.
+ * Manages inventory of mastership roles for devices, across controller
+ * instances; not intended for direct use.
  */
 public interface MastershipStore {
 
-    // three things to map: InstanceId, DeviceId, MastershipRole
+    // three things to map: NodeId, DeviceId, MastershipRole
 
     /**
      * Sets a device's role for a specified controller instance.
@@ -20,8 +21,8 @@
      * @param role     new role
      * @return a mastership event
      */
-    MastershipEvent setRole(
-            InstanceId instance, DeviceId deviceId, MastershipRole role);
+    MastershipEvent setRole(NodeId instance, DeviceId deviceId,
+                            MastershipRole role);
 
     /**
      * Adds or updates the mastership information for a device.
@@ -31,8 +32,8 @@
      * @param role     new role
      * @return a mastership event
      */
-    MastershipEvent addOrUpdateDevice(
-            InstanceId instance, DeviceId deviceId, MastershipRole role);
+    MastershipEvent addOrUpdateDevice(NodeId instance, DeviceId deviceId,
+                                      MastershipRole role);
 
     /**
      * Returns the master for a device.
@@ -40,22 +41,22 @@
      * @param deviceId the device identifier
      * @return the instance identifier of the master
      */
-    InstanceId getMaster(DeviceId deviceId);
+    NodeId getMaster(DeviceId deviceId);
 
     /**
      * Returns the devices that a controller instance is master of.
      *
-     * @param  instanceId the instance identifier
+     * @param nodeId the instance identifier
      * @return a set of device identifiers
      */
-    Set<DeviceId> getDevices(InstanceId instanceId);
+    Set<DeviceId> getDevices(NodeId nodeId);
 
     /**
      * Returns the role of a device for a specific controller instance.
      *
-     * @param instanceId the instance identifier
-     * @param deviceId the device identifiers
+     * @param nodeId the instance identifier
+     * @param deviceId   the device identifiers
      * @return the role
      */
-    MastershipRole getRole(InstanceId instanceId, DeviceId deviceId);
+    MastershipRole getRole(NodeId nodeId, DeviceId deviceId);
 }
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/NodeId.java b/core/api/src/main/java/org/onlab/onos/cluster/NodeId.java
new file mode 100644
index 0000000..2430d52
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/cluster/NodeId.java
@@ -0,0 +1,48 @@
+package org.onlab.onos.cluster;
+
+import java.util.Objects;
+
+/**
+ * Controller cluster identity.
+ */
+public class NodeId {
+
+    private final String id;
+
+    // Default constructor for serialization
+    protected NodeId() {
+        id = null;
+    }
+
+    /**
+     * Creates a new cluster node identifier from the specified string.
+     *
+     * @param id string identifier
+     */
+    public NodeId(String id) {
+        this.id = id;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof NodeId) {
+            final NodeId other = (NodeId) obj;
+            return Objects.equals(this.id, other.id);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return id;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/ConnectPoint.java b/core/api/src/main/java/org/onlab/onos/net/ConnectPoint.java
index 8f332e9..35adde9 100644
--- a/core/api/src/main/java/org/onlab/onos/net/ConnectPoint.java
+++ b/core/api/src/main/java/org/onlab/onos/net/ConnectPoint.java
@@ -66,6 +66,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof ConnectPoint) {
             final ConnectPoint other = (ConnectPoint) obj;
             return Objects.equals(this.elementId, other.elementId) &&
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultDevice.java b/core/api/src/main/java/org/onlab/onos/net/DefaultDevice.java
index c6e65f5..29a8d17 100644
--- a/core/api/src/main/java/org/onlab/onos/net/DefaultDevice.java
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultDevice.java
@@ -85,6 +85,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof DefaultDevice) {
             final DefaultDevice other = (DefaultDevice) obj;
             return Objects.equals(this.id, other.id) &&
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultHost.java b/core/api/src/main/java/org/onlab/onos/net/DefaultHost.java
index fad9147..8a2d6e6 100644
--- a/core/api/src/main/java/org/onlab/onos/net/DefaultHost.java
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultHost.java
@@ -63,6 +63,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof DefaultHost) {
             final DefaultHost other = (DefaultHost) obj;
             return Objects.equals(this.id, other.id) &&
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultLink.java b/core/api/src/main/java/org/onlab/onos/net/DefaultLink.java
index e937bf3..8771faa 100644
--- a/core/api/src/main/java/org/onlab/onos/net/DefaultLink.java
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultLink.java
@@ -53,6 +53,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof DefaultLink) {
             final DefaultLink other = (DefaultLink) obj;
             return Objects.equals(this.src, other.src) &&
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultPath.java b/core/api/src/main/java/org/onlab/onos/net/DefaultPath.java
index 63c9e88..cab41b8 100644
--- a/core/api/src/main/java/org/onlab/onos/net/DefaultPath.java
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultPath.java
@@ -57,11 +57,14 @@
 
     @Override
     public int hashCode() {
-        return 31 * super.hashCode() + Objects.hash(links);
+        return Objects.hash(links);
     }
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof DefaultPath) {
             final DefaultPath other = (DefaultPath) obj;
             return Objects.equals(this.links, other.links);
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultPort.java b/core/api/src/main/java/org/onlab/onos/net/DefaultPort.java
index 375becd..d07def8 100644
--- a/core/api/src/main/java/org/onlab/onos/net/DefaultPort.java
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultPort.java
@@ -58,6 +58,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof DefaultPort) {
             final DefaultPort other = (DefaultPort) obj;
             return Objects.equals(this.element.id(), other.element.id()) &&
diff --git a/core/api/src/main/java/org/onlab/onos/net/DeviceId.java b/core/api/src/main/java/org/onlab/onos/net/DeviceId.java
index 8d96d8b..ef8c5ab 100644
--- a/core/api/src/main/java/org/onlab/onos/net/DeviceId.java
+++ b/core/api/src/main/java/org/onlab/onos/net/DeviceId.java
@@ -7,9 +7,6 @@
  */
 public final class DeviceId extends ElementId {
 
-    // Default constructor for serialization
-    protected DeviceId() {}
-
     // Public construction is prohibited
     private DeviceId(URI uri) {
         super(uri);
diff --git a/core/api/src/main/java/org/onlab/onos/net/ElementId.java b/core/api/src/main/java/org/onlab/onos/net/ElementId.java
index a75f11e..d2cd398 100644
--- a/core/api/src/main/java/org/onlab/onos/net/ElementId.java
+++ b/core/api/src/main/java/org/onlab/onos/net/ElementId.java
@@ -40,6 +40,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof ElementId) {
             final ElementId that = (ElementId) obj;
             return this.getClass() == that.getClass() &&
diff --git a/core/api/src/main/java/org/onlab/onos/net/PortNumber.java b/core/api/src/main/java/org/onlab/onos/net/PortNumber.java
index 56e15a2..cfb11d5 100644
--- a/core/api/src/main/java/org/onlab/onos/net/PortNumber.java
+++ b/core/api/src/main/java/org/onlab/onos/net/PortNumber.java
@@ -79,6 +79,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof PortNumber) {
             final PortNumber other = (PortNumber) obj;
             return this.number == other.number;
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceStore.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceStore.java
index 851fbc1..3ed477b 100644
--- a/core/api/src/main/java/org/onlab/onos/net/device/DeviceStore.java
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceStore.java
@@ -10,7 +10,7 @@
 import java.util.List;
 
 /**
- * Manages inventory of infrastructure devices.
+ * Manages inventory of infrastructure devices; not intended for direct use.
  */
 public interface DeviceStore {
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowRule.java b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowRule.java
index d3745b4..bd4c7f3 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowRule.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultFlowRule.java
@@ -147,7 +147,6 @@
      * @see java.lang.Object#equals(java.lang.Object)
      */
     public boolean equals(Object obj) {
-
         if (this == obj) {
             return true;
         }
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStore.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStore.java
index 0698721..f00b595 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStore.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleStore.java
@@ -3,7 +3,7 @@
 import org.onlab.onos.net.DeviceId;
 
 /**
- * Manages inventory of flow rules.
+ * Manages inventory of flow rules; not intended for direct use.
  */
 public interface FlowRuleStore {
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostStore.java b/core/api/src/main/java/org/onlab/onos/net/host/HostStore.java
index 94c4585..ea316b2 100644
--- a/core/api/src/main/java/org/onlab/onos/net/host/HostStore.java
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostStore.java
@@ -12,8 +12,7 @@
 import java.util.Set;
 
 /**
- * Manages inventory of end-station hosts. It may do so using whatever
- * means are appropriate.
+ * Manages inventory of end-station hosts; not intended for direct use.
  */
 public interface HostStore {
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/link/LinkStore.java b/core/api/src/main/java/org/onlab/onos/net/link/LinkStore.java
index 4391471..dbe4877 100644
--- a/core/api/src/main/java/org/onlab/onos/net/link/LinkStore.java
+++ b/core/api/src/main/java/org/onlab/onos/net/link/LinkStore.java
@@ -8,8 +8,7 @@
 import java.util.Set;
 
 /**
- * Manages inventory of infrastructure links. It may do so using whatever
- * means are appropriate.
+ * Manages inventory of infrastructure links; not intended for direct use.
  */
 public interface LinkStore {
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/packet/DefaultInboundPacket.java b/core/api/src/main/java/org/onlab/onos/net/packet/DefaultInboundPacket.java
index 8c57adc..fb31b10 100644
--- a/core/api/src/main/java/org/onlab/onos/net/packet/DefaultInboundPacket.java
+++ b/core/api/src/main/java/org/onlab/onos/net/packet/DefaultInboundPacket.java
@@ -54,6 +54,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof InboundPacket) {
             final DefaultInboundPacket other = (DefaultInboundPacket) obj;
             return Objects.equals(this.receivedFrom, other.receivedFrom) &&
diff --git a/core/api/src/main/java/org/onlab/onos/net/provider/ProviderId.java b/core/api/src/main/java/org/onlab/onos/net/provider/ProviderId.java
index 5fc0150..725748a 100644
--- a/core/api/src/main/java/org/onlab/onos/net/provider/ProviderId.java
+++ b/core/api/src/main/java/org/onlab/onos/net/provider/ProviderId.java
@@ -12,12 +12,6 @@
     private final String scheme;
     private final String id;
 
-    // Default constructor for serialization
-    protected ProviderId() {
-        scheme = null;
-        id = null;
-    }
-
     /**
      * Creates a new provider identifier from the specified string.
      * The providers are expected to follow the reverse DNS convention, e.g.
@@ -40,6 +34,15 @@
         return scheme;
     }
 
+    /**
+     * Returns the device URI scheme specific id portion.
+     *
+     * @return id
+     */
+    public String id() {
+        return id;
+    }
+
     @Override
     public int hashCode() {
         return Objects.hash(scheme, id);
@@ -50,12 +53,12 @@
         if (this == obj) {
             return true;
         }
-        if (obj == null || getClass() != obj.getClass()) {
-            return false;
+        if (obj instanceof ProviderId) {
+            final ProviderId other = (ProviderId) obj;
+            return Objects.equals(this.scheme, other.scheme) &&
+                    Objects.equals(this.id, other.id);
         }
-        final ProviderId other = (ProviderId) obj;
-        return Objects.equals(this.scheme, other.scheme) &&
-                Objects.equals(this.id, other.id);
+        return false;
     }
 
     @Override
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/ClusterId.java b/core/api/src/main/java/org/onlab/onos/net/topology/ClusterId.java
index 502eee1..9f0e20b 100644
--- a/core/api/src/main/java/org/onlab/onos/net/topology/ClusterId.java
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/ClusterId.java
@@ -43,6 +43,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof ClusterId) {
             final ClusterId other = (ClusterId) obj;
             return Objects.equals(this.id, other.id);
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyCluster.java b/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyCluster.java
index f33dcf7..b6cc9bd 100644
--- a/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyCluster.java
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyCluster.java
@@ -59,6 +59,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof DefaultTopologyCluster) {
             final DefaultTopologyCluster other = (DefaultTopologyCluster) obj;
             return Objects.equals(this.id, other.id) &&
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyEdge.java b/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyEdge.java
index 00eb3d2..c399f3c 100644
--- a/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyEdge.java
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyEdge.java
@@ -50,6 +50,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof DefaultTopologyEdge) {
             final DefaultTopologyEdge other = (DefaultTopologyEdge) obj;
             return Objects.equals(this.link, other.link);
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyVertex.java b/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyVertex.java
index 7bc231e..4337f58 100644
--- a/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyVertex.java
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyVertex.java
@@ -32,6 +32,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof DefaultTopologyVertex) {
             final DefaultTopologyVertex other = (DefaultTopologyVertex) obj;
             return Objects.equals(this.deviceId, other.deviceId);
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyStore.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyStore.java
index 5e8b19d..adc6145 100644
--- a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyStore.java
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyStore.java
@@ -11,8 +11,7 @@
 import java.util.Set;
 
 /**
- * Manages inventory of topology snapshots. It may do so using whatever
- * means appropriate.
+ * Manages inventory of topology snapshots; not intended for direct use.
  */
 public interface TopologyStore {
 
diff --git a/core/net/src/main/java/org/onlab/onos/cluster/impl/ClusterManager.java b/core/net/src/main/java/org/onlab/onos/cluster/impl/ClusterManager.java
new file mode 100644
index 0000000..43d743c
--- /dev/null
+++ b/core/net/src/main/java/org/onlab/onos/cluster/impl/ClusterManager.java
@@ -0,0 +1,86 @@
+package org.onlab.onos.cluster.impl;
+
+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.ClusterEvent;
+import org.onlab.onos.cluster.ClusterEventListener;
+import org.onlab.onos.cluster.ClusterService;
+import org.onlab.onos.cluster.ClusterStore;
+import org.onlab.onos.cluster.ControllerNode;
+import org.onlab.onos.cluster.NodeId;
+import org.onlab.onos.event.AbstractListenerRegistry;
+import org.onlab.onos.event.EventDeliveryService;
+import org.slf4j.Logger;
+
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implementation of the cluster service.
+ */
+@Component(immediate = true)
+@Service
+public class ClusterManager implements ClusterService {
+
+    public static final String INSTANCE_ID_NULL = "Instance ID cannot be null";
+    private final Logger log = getLogger(getClass());
+
+    protected final AbstractListenerRegistry<ClusterEvent, ClusterEventListener>
+            listenerRegistry = new AbstractListenerRegistry<>();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterStore store;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected EventDeliveryService eventDispatcher;
+
+    @Activate
+    public void activate() {
+        eventDispatcher.addSink(ClusterEvent.class, listenerRegistry);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        eventDispatcher.removeSink(ClusterEvent.class);
+        log.info("Stopped");
+    }
+
+    @Override
+    public ControllerNode getLocalNode() {
+        return store.getLocalNode();
+    }
+
+    @Override
+    public Set<ControllerNode> getNodes() {
+        return store.getNodes();
+    }
+
+    @Override
+    public ControllerNode getNode(NodeId nodeId) {
+        checkNotNull(nodeId, INSTANCE_ID_NULL);
+        return store.getNode(nodeId);
+    }
+
+    @Override
+    public ControllerNode.State getState(NodeId nodeId) {
+        checkNotNull(nodeId, INSTANCE_ID_NULL);
+        return store.getState(nodeId);
+    }
+
+    @Override
+    public void addListener(ClusterEventListener listener) {
+        listenerRegistry.addListener(listener);
+    }
+
+    @Override
+    public void removeListener(ClusterEventListener listener) {
+        listenerRegistry.removeListener(listener);
+    }
+}
diff --git a/core/net/src/main/java/org/onlab/onos/cluster/impl/package-info.java b/core/net/src/main/java/org/onlab/onos/cluster/impl/package-info.java
new file mode 100644
index 0000000..d98f983
--- /dev/null
+++ b/core/net/src/main/java/org/onlab/onos/cluster/impl/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Subsystem for tracking controller cluster nodes.
+ */
+package org.onlab.onos.cluster.impl;
\ No newline at end of file
diff --git a/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java b/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java
index 5964c08..8dcba64 100644
--- a/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java
@@ -33,7 +33,7 @@
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
- * Provides basic implementation of the device SB &amp; NB APIs.
+ * Provides implementation of the device SB &amp; NB APIs.
  */
 @Component(immediate = true)
 @Service
diff --git a/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java b/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
index 51ea328..7b11798 100644
--- a/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
@@ -33,6 +33,9 @@
 
 import com.google.common.collect.Lists;
 
+/**
+ * Provides implementation of the flow NB &amp; SB APIs.
+ */
 @Component(immediate = true)
 @Service
 public class FlowRuleManager
diff --git a/core/net/src/test/java/org/onlab/onos/net/device/impl/DistributedDeviceManagerTest.java b/core/net/src/test/java/org/onlab/onos/net/device/impl/DistributedDeviceManagerTest.java
index a928572..222216c 100644
--- a/core/net/src/test/java/org/onlab/onos/net/device/impl/DistributedDeviceManagerTest.java
+++ b/core/net/src/test/java/org/onlab/onos/net/device/impl/DistributedDeviceManagerTest.java
@@ -112,7 +112,7 @@
         mgr.deactivate();
 
         dstore.deactivate();
-        dstore.theInstance.shutdown();
+        ((TestDistributedDeviceStore) dstore).shutdownHz();
     }
 
     private void connectDevice(DeviceId deviceId, String swVersion) {
@@ -290,5 +290,12 @@
                 }
             };
         }
+
+        /**
+         * Shutdowns the hazelcast instance.
+         */
+        public void shutdownHz() {
+            theInstance.shutdown();
+        }
     }
 }
diff --git a/core/store/src/main/java/org/onlab/onos/store/cluster/impl/DistributedClusterStore.java b/core/store/src/main/java/org/onlab/onos/store/cluster/impl/DistributedClusterStore.java
new file mode 100644
index 0000000..ee09570
--- /dev/null
+++ b/core/store/src/main/java/org/onlab/onos/store/cluster/impl/DistributedClusterStore.java
@@ -0,0 +1,87 @@
+package org.onlab.onos.store.cluster.impl;
+
+import com.google.common.collect.ImmutableSet;
+import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.core.Member;
+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.ClusterStore;
+import org.onlab.onos.cluster.ControllerNode;
+import org.onlab.onos.cluster.DefaultControllerNode;
+import org.onlab.onos.cluster.NodeId;
+import org.onlab.onos.store.StoreService;
+import org.onlab.packet.IpPrefix;
+import org.slf4j.Logger;
+
+import java.util.Set;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Distributed implementation of the cluster nodes store.
+ */
+@Component(immediate = true)
+@Service
+public class DistributedClusterStore implements ClusterStore {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected StoreService storeService;
+
+    private HazelcastInstance theInstance;
+
+    // FIXME: experimental implementation; enhance to assure persistence and
+    // visibility to nodes that are not currently in the cluster
+
+    @Activate
+    public void activate() {
+        log.info("Started");
+        theInstance = storeService.getHazelcastInstance();
+
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+    @Override
+    public ControllerNode getLocalNode() {
+        return node(theInstance.getCluster().getLocalMember());
+    }
+
+    @Override
+    public Set<ControllerNode> getNodes() {
+        ImmutableSet.Builder<ControllerNode> builder = ImmutableSet.builder();
+        for (Member member : theInstance.getCluster().getMembers()) {
+            builder.add(node(member));
+        }
+        return builder.build();
+    }
+
+    @Override
+    public ControllerNode getNode(NodeId nodeId) {
+        for (Member member : theInstance.getCluster().getMembers()) {
+            if (member.getUuid().equals(nodeId.toString())) {
+                return node(member);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public ControllerNode.State getState(NodeId nodeId) {
+        return ControllerNode.State.ACTIVE;
+    }
+
+    // Creates a controller node descriptor from the Hazelcast member.
+    private ControllerNode node(Member member) {
+        return new DefaultControllerNode(new NodeId(member.getUuid()),
+                                         IpPrefix.valueOf(member.getSocketAddress().getAddress().getAddress()));
+    }
+}
diff --git a/core/store/src/main/java/org/onlab/onos/store/cluster/impl/package-info.java b/core/store/src/main/java/org/onlab/onos/store/cluster/impl/package-info.java
new file mode 100644
index 0000000..fe3df5d
--- /dev/null
+++ b/core/store/src/main/java/org/onlab/onos/store/cluster/impl/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Implementation of a distributed cluster node store using Hazelcast.
+ */
+package org.onlab.onos.store.cluster.impl;
\ No newline at end of file
diff --git a/core/store/src/main/java/org/onlab/onos/store/device/impl/DefaultPortSerializer.java b/core/store/src/main/java/org/onlab/onos/store/device/impl/DefaultPortSerializer.java
index 4a60eb8..bd0aa06 100644
--- a/core/store/src/main/java/org/onlab/onos/store/device/impl/DefaultPortSerializer.java
+++ b/core/store/src/main/java/org/onlab/onos/store/device/impl/DefaultPortSerializer.java
@@ -16,6 +16,9 @@
 import com.google.common.collect.ImmutableSet;
 
 // TODO move to util, etc.
+/**
+ * Kryo Serializer for {@link DefaultPort}.
+ */
 public final class DefaultPortSerializer extends
         Serializer<DefaultPort> {
 
@@ -23,6 +26,9 @@
         = new CollectionSerializer(IpPrefix.class,
                             new IpPrefixSerializer(), false);
 
+    /**
+     * Default constructor.
+     */
     public DefaultPortSerializer() {
         // non-null, immutable
         super(false, true);
diff --git a/core/store/src/main/java/org/onlab/onos/store/device/impl/DistributedDeviceStore.java b/core/store/src/main/java/org/onlab/onos/store/device/impl/DistributedDeviceStore.java
index b8c1cdf..fe03cda 100644
--- a/core/store/src/main/java/org/onlab/onos/store/device/impl/DistributedDeviceStore.java
+++ b/core/store/src/main/java/org/onlab/onos/store/device/impl/DistributedDeviceStore.java
@@ -44,6 +44,10 @@
 import org.onlab.util.KryoPool;
 import org.slf4j.Logger;
 
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
 import com.google.common.base.Optional;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
@@ -68,103 +72,26 @@
 @Service
 public class DistributedDeviceStore implements DeviceStore {
 
-    /**
-     * An IMap EntryListener, which reflects each remote event to cache.
-     *
-     * @param <K> IMap key type after deserialization
-     * @param <V> IMap value type after deserialization
-     */
-    public static final class RemoteEventHandler<K, V> extends
-            EntryAdapter<byte[], byte[]> {
-
-        private LoadingCache<K, Optional<V>> cache;
-
-        /**
-         * Constructor.
-         *
-         * @param cache cache to update
-         */
-        public RemoteEventHandler(
-                LoadingCache<K, Optional<V>> cache) {
-            this.cache = checkNotNull(cache);
-        }
-
-        @Override
-        public void mapCleared(MapEvent event) {
-            cache.invalidateAll();
-        }
-
-        @Override
-        public void entryUpdated(EntryEvent<byte[], byte[]> event) {
-            cache.put(POOL.<K>deserialize(event.getKey()),
-                        Optional.of(POOL.<V>deserialize(
-                                        event.getValue())));
-        }
-
-        @Override
-        public void entryRemoved(EntryEvent<byte[], byte[]> event) {
-            cache.invalidate(POOL.<DeviceId>deserialize(event.getKey()));
-        }
-
-        @Override
-        public void entryAdded(EntryEvent<byte[], byte[]> event) {
-            entryUpdated(event);
-        }
-    }
-
-    /**
-     * CacheLoader to wrap Map value with Optional,
-     * to handle negative hit on underlying IMap.
-     *
-     * @param <K> IMap key type after deserialization
-     * @param <V> IMap value type after deserialization
-     */
-    public static final class OptionalCacheLoader<K, V> extends
-            CacheLoader<K, Optional<V>> {
-
-        private IMap<byte[], byte[]> rawMap;
-
-        /**
-         * Constructor.
-         *
-         * @param rawMap underlying IMap
-         */
-        public OptionalCacheLoader(IMap<byte[], byte[]> rawMap) {
-            this.rawMap = checkNotNull(rawMap);
-        }
-
-        @Override
-        public Optional<V> load(K key) throws Exception {
-            byte[] keyBytes = serialize(key);
-            byte[] valBytes = rawMap.get(keyBytes);
-            if (valBytes == null) {
-                return Optional.absent();
-            }
-            V dev = deserialize(valBytes);
-            return Optional.of(dev);
-        }
-    }
-
     private final Logger log = getLogger(getClass());
 
     public static final String DEVICE_NOT_FOUND = "Device with ID %s not found";
 
     // FIXME Slice out types used in common to separate pool/namespace.
     private static final KryoPool POOL = KryoPool.newBuilder()
-            .register(URI.class, new URISerializer())
             .register(
                     ArrayList.class,
+                    HashMap.class,
 
-                    ProviderId.class,
                     Device.Type.class,
 
-                    DeviceId.class,
                     DefaultDevice.class,
                     MastershipRole.class,
-                    HashMap.class,
                     Port.class,
                     Element.class
                     )
+            .register(URI.class, new URISerializer())
+            .register(ProviderId.class, new ProviderIdSerializer())
+            .register(DeviceId.class, new DeviceIdSerializer())
             .register(PortNumber.class, new PortNumberSerializer())
             .register(DefaultPort.class, new DefaultPortSerializer())
             .build()
@@ -190,7 +117,7 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected StoreService storeService;
 
-    /*protected*/public HazelcastInstance theInstance;
+    protected HazelcastInstance theInstance;
 
 
     @Activate
@@ -517,4 +444,94 @@
         return POOL.deserialize(bytes);
     }
 
+    public static final class DeviceIdSerializer extends Serializer<DeviceId> {
+
+        @Override
+        public void write(Kryo kryo, Output output, DeviceId object) {
+            kryo.writeObject(output, object.uri());
+        }
+
+        @Override
+        public DeviceId read(Kryo kryo, Input input, Class<DeviceId> type) {
+            final URI uri = kryo.readObject(input, URI.class);
+            return DeviceId.deviceId(uri);
+        }
+    }
+
+    /**
+     * An IMap EntryListener, which reflects each remote event to cache.
+     *
+     * @param <K> IMap key type after deserialization
+     * @param <V> IMap value type after deserialization
+     */
+    public static final class RemoteEventHandler<K, V> extends
+            EntryAdapter<byte[], byte[]> {
+
+        private LoadingCache<K, Optional<V>> cache;
+
+        /**
+         * Constructor.
+         *
+         * @param cache cache to update
+         */
+        public RemoteEventHandler(
+                LoadingCache<K, Optional<V>> cache) {
+            this.cache = checkNotNull(cache);
+        }
+
+        @Override
+        public void mapCleared(MapEvent event) {
+            cache.invalidateAll();
+        }
+
+        @Override
+        public void entryUpdated(EntryEvent<byte[], byte[]> event) {
+            cache.put(POOL.<K>deserialize(event.getKey()),
+                        Optional.of(POOL.<V>deserialize(
+                                        event.getValue())));
+        }
+
+        @Override
+        public void entryRemoved(EntryEvent<byte[], byte[]> event) {
+            cache.invalidate(POOL.<DeviceId>deserialize(event.getKey()));
+        }
+
+        @Override
+        public void entryAdded(EntryEvent<byte[], byte[]> event) {
+            entryUpdated(event);
+        }
+    }
+
+    /**
+     * CacheLoader to wrap Map value with Optional,
+     * to handle negative hit on underlying IMap.
+     *
+     * @param <K> IMap key type after deserialization
+     * @param <V> IMap value type after deserialization
+     */
+    public static final class OptionalCacheLoader<K, V> extends
+            CacheLoader<K, Optional<V>> {
+
+        private IMap<byte[], byte[]> rawMap;
+
+        /**
+         * Constructor.
+         *
+         * @param rawMap underlying IMap
+         */
+        public OptionalCacheLoader(IMap<byte[], byte[]> rawMap) {
+            this.rawMap = checkNotNull(rawMap);
+        }
+
+        @Override
+        public Optional<V> load(K key) throws Exception {
+            byte[] keyBytes = serialize(key);
+            byte[] valBytes = rawMap.get(keyBytes);
+            if (valBytes == null) {
+                return Optional.absent();
+            }
+            V dev = deserialize(valBytes);
+            return Optional.of(dev);
+        }
+    }
 }
diff --git a/core/store/src/main/java/org/onlab/onos/store/device/impl/IpPrefixSerializer.java b/core/store/src/main/java/org/onlab/onos/store/device/impl/IpPrefixSerializer.java
index 3d3efe0..d6296a3 100644
--- a/core/store/src/main/java/org/onlab/onos/store/device/impl/IpPrefixSerializer.java
+++ b/core/store/src/main/java/org/onlab/onos/store/device/impl/IpPrefixSerializer.java
@@ -8,8 +8,14 @@
 import com.esotericsoftware.kryo.io.Output;
 
 // TODO move to util, etc.
+/**
+ * Kryo Serializer for {@link IpPrefix}.
+ */
 public final class IpPrefixSerializer extends Serializer<IpPrefix> {
 
+    /**
+     * Default constructor.
+     */
     public IpPrefixSerializer() {
         // non-null, immutable
         super(false, true);
diff --git a/core/store/src/main/java/org/onlab/onos/store/device/impl/PortNumberSerializer.java b/core/store/src/main/java/org/onlab/onos/store/device/impl/PortNumberSerializer.java
index 4483187..ae11867 100644
--- a/core/store/src/main/java/org/onlab/onos/store/device/impl/PortNumberSerializer.java
+++ b/core/store/src/main/java/org/onlab/onos/store/device/impl/PortNumberSerializer.java
@@ -8,9 +8,15 @@
 import com.esotericsoftware.kryo.io.Output;
 
 // TODO move to util, etc.
+/**
+ * Serializer for {@link PortNumber}.
+ */
 public final class PortNumberSerializer extends
         Serializer<PortNumber> {
 
+    /**
+     * Default constructor.
+     */
     public PortNumberSerializer() {
         // non-null, immutable
         super(false, true);
diff --git a/core/store/src/main/java/org/onlab/onos/store/device/impl/ProviderIdSerializer.java b/core/store/src/main/java/org/onlab/onos/store/device/impl/ProviderIdSerializer.java
new file mode 100644
index 0000000..82b7a70
--- /dev/null
+++ b/core/store/src/main/java/org/onlab/onos/store/device/impl/ProviderIdSerializer.java
@@ -0,0 +1,37 @@
+package org.onlab.onos.store.device.impl;
+
+import org.onlab.onos.net.provider.ProviderId;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+//TODO move to util, etc.
+/**
+ * Serializer for {@link ProviderId}.
+ */
+public class ProviderIdSerializer extends Serializer<ProviderId> {
+
+    /**
+     * Default constructor.
+     */
+    public ProviderIdSerializer() {
+        // non-null, immutable
+        super(false, true);
+    }
+
+    @Override
+    public void write(Kryo kryo, Output output, ProviderId object) {
+        output.writeString(object.scheme());
+        output.writeString(object.id());
+    }
+
+    @Override
+    public ProviderId read(Kryo kryo, Input input, Class<ProviderId> type) {
+        String scheme = input.readString();
+        String id = input.readString();
+        return new ProviderId(scheme, id);
+    }
+
+}
diff --git a/core/store/src/main/java/org/onlab/onos/store/package-info.java b/core/store/src/main/java/org/onlab/onos/store/package-info.java
new file mode 100644
index 0000000..e0b6956
--- /dev/null
+++ b/core/store/src/main/java/org/onlab/onos/store/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Common abstractions and facilities for implementing distributed store
+ * using Hazelcast.
+ */
+package org.onlab.onos.store;
\ No newline at end of file
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/PathKey.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/PathKey.java
index 8030823..da3b055 100644
--- a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/PathKey.java
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/PathKey.java
@@ -28,6 +28,9 @@
 
     @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);
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleClusterStore.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleClusterStore.java
new file mode 100644
index 0000000..782b389
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleClusterStore.java
@@ -0,0 +1,65 @@
+package org.onlab.onos.net.trivial.impl;
+
+import com.google.common.collect.ImmutableSet;
+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.cluster.ClusterStore;
+import org.onlab.onos.cluster.ControllerNode;
+import org.onlab.onos.cluster.DefaultControllerNode;
+import org.onlab.onos.cluster.NodeId;
+import org.onlab.packet.IpPrefix;
+import org.slf4j.Logger;
+
+import java.util.Set;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Manages inventory of infrastructure DEVICES using trivial in-memory
+ * structures implementation.
+ */
+@Component(immediate = true)
+@Service
+public class SimpleClusterStore implements ClusterStore {
+
+    public static final IpPrefix LOCALHOST = IpPrefix.valueOf("127.0.0.1");
+
+    private final Logger log = getLogger(getClass());
+
+    private ControllerNode instance;
+
+    @Activate
+    public void activate() {
+        instance = new DefaultControllerNode(new NodeId("local"), LOCALHOST);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+
+    @Override
+    public ControllerNode getLocalNode() {
+        return instance;
+    }
+
+    @Override
+    public Set<ControllerNode> getNodes() {
+        return ImmutableSet.of(instance);
+    }
+
+    @Override
+    public ControllerNode getNode(NodeId nodeId) {
+        return instance.id().equals(nodeId) ? instance : null;
+    }
+
+    @Override
+    public ControllerNode.State getState(NodeId nodeId) {
+        return ControllerNode.State.ACTIVE;
+    }
+
+}
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleDeviceStore.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleDeviceStore.java
index 5441c6d..8e42ffb 100644
--- a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleDeviceStore.java
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleDeviceStore.java
@@ -35,7 +35,7 @@
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
- * Manages inventory of infrastructure DEVICES using trivial in-memory
+ * Manages inventory of infrastructure devices using trivial in-memory
  * structures implementation.
  */
 @Component(immediate = true)
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleLinkStore.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleLinkStore.java
index 3792fd6..5c99682 100644
--- a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleLinkStore.java
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleLinkStore.java
@@ -179,6 +179,9 @@
 
         @Override
         public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
             if (obj instanceof LinkKey) {
                 final LinkKey other = (LinkKey) obj;
                 return Objects.equals(this.src, other.src) &&
diff --git a/pom.xml b/pom.xml
index 0e51910..21a8aef 100644
--- a/pom.xml
+++ b/pom.xml
@@ -392,7 +392,7 @@
                         <group>
                             <title>Core Subsystems</title>
                             <packages>
-                                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.net.trivial.*:org.onlab.onos.net.*.impl:org.onlab.onos.impl:org.onlab.onos.event.impl:org.onlab.onos.store.*
+                                org.onlab.onos.cluster.impl:org.onlab.onos.store: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.net.trivial.*:org.onlab.onos.net.*.impl:org.onlab.onos.cluster:org.onlab.onos.event.impl:org.onlab.onos.store.*
                             </packages>
                         </group>
                         <group>
diff --git a/tools/build/envDefaults b/tools/build/envDefaults
index 71d4cf4..473095c 100644
--- a/tools/build/envDefaults
+++ b/tools/build/envDefaults
@@ -6,6 +6,7 @@
 # M2 repository and Karaf gold bits
 export M2_REPO=${M2_REPO:-~/.m2/repository}
 export KARAF_ZIP=${KARAF_ZIP:-~/Downloads/apache-karaf-3.0.1.zip}
+export KARAF_TAR=${KARAF_TAR:-~/Downloads/apache-karaf-3.0.1.tar.gz}
 export KARAF_DIST=$(basename $KARAF_ZIP .zip)
 
 # ONOS Version and onos.tar.gz staging environment
diff --git a/tools/build/onos-package b/tools/build/onos-package
index a35308e..03ebce6 100755
--- a/tools/build/onos-package
+++ b/tools/build/onos-package
@@ -13,7 +13,7 @@
 
 # Make sure we have the original apache karaf bits first
 [ ! -d $M2_REPO ] && echo "M2 repository $M2_REPO not found" && exit 1
-[ ! -f $KARAF_ZIP ] && echo "Apache Karaf bits $KARAF_ZIP not found" && exit 1
+[ ! -f $KARAF_ZIP -a ! -f $KARAF_TAR ] && echo "Apache Karaf bits $KARAF_ZIP or $KARAF_TAR not found" && exit 1
 [ -d $ONOS_STAGE ] && echo "ONOS stage $ONOS_STAGE already exists" && exit 1
 
 # Create the stage directory and warp into it
@@ -21,7 +21,8 @@
 cd $ONOS_STAGE
 
 # Unroll the Apache Karaf bits, prune them and make ONOS top-level directories.
-unzip -q $KARAF_ZIP && rm -rf $KARAF_DIST/demos
+[ -f $KARAF_ZIP ] && unzip -q $KARAF_ZIP && rm -rf $KARAF_DIST/demos
+[ -f $KARAF_TAR ] && tar zxf $KARAF_TAR && rm -rf $KARAF_DIST/demos
 mkdir bin
 
 # Stage the ONOS admin scripts and patch in Karaf service wrapper extras
@@ -33,15 +34,15 @@
 mkdir -p $KARAF_DIST/system/org/onlab 
 cp -r $M2_REPO/org/onlab $KARAF_DIST/system/org/
 
-# Wrapper & Cellar Patching ----------------------------------------------------
+# Cellar Patching --------------------------------------------------------------
 
 # Patch the Apache Karaf distribution file to add Cellar features repository
-perl -pi.old -e "s|^(featuresRepositories=.*)|\1,mvn:org.apache.karaf.cellar/apache-karaf-cellar/3.0.0/xml/features|" \
-    $ONOS_STAGE/$KARAF_DIST/etc/org.apache.karaf.features.cfg 
+#perl -pi.old -e "s|^(featuresRepositories=.*)|\1,mvn:org.apache.karaf.cellar/apache-karaf-cellar/3.0.0/xml/features|" \
+#    $ONOS_STAGE/$KARAF_DIST/etc/org.apache.karaf.features.cfg
 
 # Patch the Apache Karaf distribution file to load ONOS features
-perl -pi.old -e 's|^(featuresBoot=.*)|\1,cellar|' \
-    $ONOS_STAGE/$KARAF_DIST/etc/org.apache.karaf.features.cfg 
+#perl -pi.old -e 's|^(featuresBoot=.*)|\1,cellar|' \
+#    $ONOS_STAGE/$KARAF_DIST/etc/org.apache.karaf.features.cfg
 
 # ONOS Patching ----------------------------------------------------------------
 
@@ -49,6 +50,10 @@
 perl -pi.old -e "s|^(featuresRepositories=.*)|\1,mvn:org.onlab.onos/onos-features/$ONOS_VERSION/xml/features|" \
     $ONOS_STAGE/$KARAF_DIST/etc/org.apache.karaf.features.cfg 
 
+# Patch the Apache Karaf distribution file to load ONOS features
+perl -pi.old -e 's|^(featuresBoot=.*)|\1,onos-api,onos-core,onos-cli,onos-rest,onos-gui,onos-openflow,onos-app-tvue,onos-app-fwd|' \
+    $ONOS_STAGE/$KARAF_DIST/etc/org.apache.karaf.features.cfg
+
 # Patch the Apache Karaf distribution with ONOS branding bundle
 cp $M2_REPO/org/onlab/onos/onos-branding/$ONOS_VERSION/onos-branding-*.jar \
     $ONOS_STAGE/$KARAF_DIST/lib
diff --git a/tools/dev/bash_profile b/tools/dev/bash_profile
index a863004..8304bb2 100644
--- a/tools/dev/bash_profile
+++ b/tools/dev/bash_profile
@@ -46,10 +46,7 @@
 # Test related conveniences
 
 # SSH to a specified ONOS instance
-function sshctl {
-    [ -n "$1" ] && OCI=$1 && shift
-    ssh -Y sdn@$OCI "$@"
-}
+alias sshctl=onos-ssh
 
 # Applies the settings in the specified cell file or lists current cell definition
 # if no cell file is given.
diff --git a/tools/test/bin/onos-config b/tools/test/bin/onos-config
index 11e7349..dd03e2d 100755
--- a/tools/test/bin/onos-config
+++ b/tools/test/bin/onos-config
@@ -21,7 +21,8 @@
 
     echo 'Installing ONOS bundles...'
     $onos cluster:feature-install default onos-api 1>>$LOG 2>&1
-    $onos cluster:feature-install default onos-core 1>>$LOG 2>&1
+  # $onos cluster:feature-install default onos-core 1>>$LOG 2>&1
+    $onos cluster:feature-install default onos-core-trivial 1>>$LOG 2>&1
     $onos cluster:feature-install default onos-openflow 1>>$LOG 2>&1
     $onos cluster:feature-install default onos-cli 1>>$LOG 2>&1
   # $onos cluster:feature-install default onos-gui 1>>$LOG 2>&1
diff --git a/tools/test/bin/onos-install b/tools/test/bin/onos-install
index 6454ee0..b270704 100755
--- a/tools/test/bin/onos-install
+++ b/tools/test/bin/onos-install
@@ -27,6 +27,9 @@
     # Install the upstart configuration file.
     sudo cp $ONOS_INSTALL_DIR/debian/onos.conf /etc/init/onos.conf
 
+    # Remove any previous ON.Lab bits from ~/.m2 repo
+    rm -fr ~/.m2/repository/org/onlab
+
     # Ignite the ONOS service.
     sudo service onos start
 "
diff --git a/tools/test/bin/onos-patch-vm b/tools/test/bin/onos-patch-vm
new file mode 100755
index 0000000..60252c0
--- /dev/null
+++ b/tools/test/bin/onos-patch-vm
@@ -0,0 +1,20 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# Remotely patches the ONOS VM to tailor its hostname.
+#-------------------------------------------------------------------------------
+
+[ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
+. $ONOS_ROOT/tools/build/envDefaults
+
+address=${1:-$OCI}
+remote=$ONOS_USER@$address
+name=${2:-onos-1}
+
+[ -z "$address" ] && echo "Null address not allowed" >&2 && exit 1
+[ -z "$name" ] && echo "Null name not allowed" >&2 && exit 1
+
+ssh $remote "
+    sudo perl -pi.bak -e \"s/127.0.1.1.*/127.0.1.1       $name/g\" /etc/hosts
+    sudo echo $name /etc/hostname
+    sudo hostname $name
+"
\ No newline at end of file
diff --git a/tools/test/bin/onos-push-keys b/tools/test/bin/onos-push-keys
new file mode 100755
index 0000000..a469643
--- /dev/null
+++ b/tools/test/bin/onos-push-keys
@@ -0,0 +1,13 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# Pushes the local id_rsa.pub to the remote ONOS host authorized_keys.
+#-------------------------------------------------------------------------------
+
+[ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
+. $ONOS_ROOT/tools/build/envDefaults
+
+remote=$ONOS_USER@${1:-$OCI}
+
+scp -q ~/.ssh/id_rsa.pub $remote:/tmp
+ssh $remote "cat /tmp/id_rsa.pub >> ~/.ssh/authorized_keys"
+ssh -n -o PasswordAuthentication=no $remote true
diff --git a/tools/test/bin/onos-ssh b/tools/test/bin/onos-ssh
new file mode 100755
index 0000000..406c9cb
--- /dev/null
+++ b/tools/test/bin/onos-ssh
@@ -0,0 +1,10 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# Logs in to the remote ONOS instance.
+#-------------------------------------------------------------------------------
+
+[ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
+. $ONOS_ROOT/tools/build/envDefaults
+
+[ -n "$1" ] && OCI=$1 && shift
+ssh -Y $ONOS_USER@$OCI "$@"
diff --git a/tools/test/bin/onos-verify-cell b/tools/test/bin/onos-verify-cell
new file mode 100755
index 0000000..8e7fe99
--- /dev/null
+++ b/tools/test/bin/onos-verify-cell
@@ -0,0 +1,11 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# Verifies connectivity to each node in ONOS cell.
+#-------------------------------------------------------------------------------
+
+[ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
+. $ONOS_ROOT/tools/build/envDefaults
+
+for node in $(env | sort | egrep "OC[0-9]+" | cut -d= -f2); do
+    printf "%s: " $node; ssh -n -o PasswordAuthentication=no $ONOS_USER@$node date
+done
\ No newline at end of file
diff --git a/tools/test/bin/onos-wait-for-start b/tools/test/bin/onos-wait-for-start
new file mode 100755
index 0000000..0190d6f
--- /dev/null
+++ b/tools/test/bin/onos-wait-for-start
@@ -0,0 +1,19 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# Waits for ONOS to reach run-level 100.
+#-------------------------------------------------------------------------------
+
+[ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
+. $ONOS_ROOT/tools/build/envDefaults
+
+remote=$ONOS_USER@${1:-$OCI}
+
+ssh $remote "
+    # Wait until we reach the run-level 100
+    running=""
+    while [ -z \$running ]; do
+        $ONOS_INSTALL_DIR/bin/onos bundle:list 2>/dev/null | \
+            grep -q 'START LEVEL 100' && running=1 || sleep 2
+    done
+
+"
diff --git a/utils/misc/src/main/java/org/onlab/graph/AbstractEdge.java b/utils/misc/src/main/java/org/onlab/graph/AbstractEdge.java
index 163a147..4d1546b 100644
--- a/utils/misc/src/main/java/org/onlab/graph/AbstractEdge.java
+++ b/utils/misc/src/main/java/org/onlab/graph/AbstractEdge.java
@@ -41,6 +41,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof AbstractEdge) {
             final AbstractEdge other = (AbstractEdge) obj;
             return Objects.equals(this.src, other.src) && Objects.equals(this.dst, other.dst);
diff --git a/utils/misc/src/main/java/org/onlab/graph/AdjacencyListsGraph.java b/utils/misc/src/main/java/org/onlab/graph/AdjacencyListsGraph.java
index a0d569b..981d09b 100644
--- a/utils/misc/src/main/java/org/onlab/graph/AdjacencyListsGraph.java
+++ b/utils/misc/src/main/java/org/onlab/graph/AdjacencyListsGraph.java
@@ -80,6 +80,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof AdjacencyListsGraph) {
             AdjacencyListsGraph that = (AdjacencyListsGraph) obj;
             return this.getClass() == that.getClass() &&
diff --git a/utils/misc/src/main/java/org/onlab/graph/DefaultMutablePath.java b/utils/misc/src/main/java/org/onlab/graph/DefaultMutablePath.java
index 401e9e9..09a20a4 100644
--- a/utils/misc/src/main/java/org/onlab/graph/DefaultMutablePath.java
+++ b/utils/misc/src/main/java/org/onlab/graph/DefaultMutablePath.java
@@ -107,6 +107,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof DefaultMutablePath) {
             final DefaultMutablePath other = (DefaultMutablePath) obj;
             return Objects.equals(this.cost, other.cost) &&
diff --git a/utils/misc/src/main/java/org/onlab/graph/DefaultPath.java b/utils/misc/src/main/java/org/onlab/graph/DefaultPath.java
index a43dd0f..a4fc386 100644
--- a/utils/misc/src/main/java/org/onlab/graph/DefaultPath.java
+++ b/utils/misc/src/main/java/org/onlab/graph/DefaultPath.java
@@ -72,6 +72,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof DefaultPath) {
             final DefaultPath other = (DefaultPath) obj;
             return Objects.equals(this.src, other.src) &&
diff --git a/utils/misc/src/main/java/org/onlab/graph/Heap.java b/utils/misc/src/main/java/org/onlab/graph/Heap.java
index ecc4759..2f91083 100644
--- a/utils/misc/src/main/java/org/onlab/graph/Heap.java
+++ b/utils/misc/src/main/java/org/onlab/graph/Heap.java
@@ -166,6 +166,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof Heap) {
             Heap that = (Heap) obj;
             return this.getClass() == that.getClass() &&
diff --git a/utils/misc/src/test/java/org/onlab/graph/TestEdge.java b/utils/misc/src/test/java/org/onlab/graph/TestEdge.java
index 0accab3..aa85d88 100644
--- a/utils/misc/src/test/java/org/onlab/graph/TestEdge.java
+++ b/utils/misc/src/test/java/org/onlab/graph/TestEdge.java
@@ -39,6 +39,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof TestEdge) {
             final TestEdge other = (TestEdge) obj;
             return super.equals(obj) && Objects.equals(this.weight, other.weight);
diff --git a/utils/misc/src/test/java/org/onlab/graph/TestVertex.java b/utils/misc/src/test/java/org/onlab/graph/TestVertex.java
index 2e960f3..ca6890f 100644
--- a/utils/misc/src/test/java/org/onlab/graph/TestVertex.java
+++ b/utils/misc/src/test/java/org/onlab/graph/TestVertex.java
@@ -20,6 +20,9 @@
 
     @Override
     public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
         if (obj instanceof TestVertex) {
             final TestVertex other = (TestVertex) obj;
             return Objects.equals(this.name, other.name);