add roleInfo structure and backup_changed mastership event

Change-Id: Iedee219fe250d681377d73a50a71f5fa72cd7802
diff --git a/core/api/src/main/java/org/onlab/onos/mastership/MastershipEvent.java b/core/api/src/main/java/org/onlab/onos/mastership/MastershipEvent.java
index bd7462d..7b8531c 100644
--- a/core/api/src/main/java/org/onlab/onos/mastership/MastershipEvent.java
+++ b/core/api/src/main/java/org/onlab/onos/mastership/MastershipEvent.java
@@ -11,7 +11,7 @@
 
     //do we worry about explicitly setting slaves/equals? probably not,
     //to keep it simple
-    NodeId master;
+    NodeId node;
 
     /**
      * Type of mastership events.
@@ -20,7 +20,12 @@
         /**
          * Signifies that the master for a device has changed.
          */
-        MASTER_CHANGED
+        MASTER_CHANGED,
+
+        /**
+         * Signifies that the list of backup nodes has changed.
+         */
+        BACKUPS_CHANGED
     }
 
     /**
@@ -29,11 +34,11 @@
      *
      * @param type   device event type
      * @param device event device subject
-     * @param master master ID subject
+     * @param node master ID subject
      */
-    public MastershipEvent(Type type, DeviceId device, NodeId master) {
+    public MastershipEvent(Type type, DeviceId device, NodeId node) {
         super(type, device);
-        this.master = master;
+        this.node = node;
     }
 
     /**
@@ -47,15 +52,15 @@
      */
     public MastershipEvent(Type type, DeviceId device, NodeId master, long time) {
         super(type, device, time);
-        this.master = master;
+        this.node = master;
     }
 
     /**
-     * Returns the current master's ID as a subject.
+     * Returns the NodeID of the node responsible for triggering the event.
      *
-     * @return master ID subject
+     * @return node ID as a subject
      */
-    public NodeId master() {
-        return master;
+    public NodeId node() {
+        return node;
     }
 }
diff --git a/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java b/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
index 7cf14fc..867c6cb 100644
--- a/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
+++ b/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
@@ -228,8 +228,8 @@
                 return true;
             }
             //else {
-                //FIXME: break tie for equal-sized clusters,
-                //       maybe by number of connected switches
+                //FIXME: break tie for equal-sized clusters, by number of
+                //       connected switches, then masters, then nodeId hash
             // }
             return false;
         }
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 b163f08..8ee6f55 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
@@ -367,7 +367,7 @@
             final NodeId myNodeId = clusterService.getLocalNode().id();
 
             log.info("## got Mastershipevent for dev {}", did);
-            if (myNodeId.equals(event.master())) {
+            if (myNodeId.equals(event.node())) {
                 MastershipTerm term = termService.getMastershipTerm(did);
 
                 if (!myNodeId.equals(term.master())) {
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
index dcbdb4a..5e1457a 100644
--- 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
@@ -86,7 +86,7 @@
             final List<NodeId> standbyList = Collections.<NodeId>emptyList();
             eventDispatcher.post(new ReplicaInfoEvent(MASTER_CHANGED,
                                 event.subject(),
-                                new ReplicaInfo(event.master(), standbyList)));
+                                new ReplicaInfo(event.node(), standbyList)));
         }
     }
 
diff --git a/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java b/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
index 15f80f8..28ba049 100644
--- a/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
+++ b/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
@@ -79,9 +79,9 @@
         };
 
         roleMap = new SMap(theInstance.getMap("nodeRoles"), this.serializer);
+        roleMap.addEntryListener((new RemoteMasterShipEventHandler()), true);
         terms = new SMap(theInstance.getMap("terms"), this.serializer);
         clusterSize = theInstance.getAtomicLong("clustersize");
-        roleMap.addEntryListener((new RemoteMasterShipEventHandler()), true);
 
         log.info("Started");
     }
diff --git a/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/RoleValue.java b/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/RoleValue.java
index f2c3559..51dd293 100644
--- a/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/RoleValue.java
+++ b/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/RoleValue.java
@@ -8,6 +8,7 @@
 
 import org.onlab.onos.cluster.NodeId;
 import org.onlab.onos.net.MastershipRole;
+import org.onlab.onos.store.common.RoleInfo;
 
 /**
  * A structure that holds node mastership roles associated with a
@@ -77,7 +78,6 @@
      * @param from the old role
      * @param to the new role
      */
-    // might want to add anyways as default behavior
     public void reassign(NodeId nodeId, MastershipRole from, MastershipRole to) {
         remove(from, nodeId);
         add(to, nodeId);
@@ -91,12 +91,22 @@
      * @param to the new NodeId
      * @param type the role associated with the old NodeId
      */
-    // might want to add anyways as default behavior
     public void replace(NodeId from, NodeId to, MastershipRole type) {
         remove(type, from);
         add(type, to);
     }
 
+    /**
+     * Summarizes this RoleValue as a RoleInfo. Note that master and/or backups
+     * may be empty, so the values should be checked for safety.
+     *
+     * @return the RoleInfo.
+     */
+    public RoleInfo roleInfo() {
+        return new RoleInfo(
+                get(MastershipRole.MASTER), nodesOfRole(MastershipRole.STANDBY));
+    }
+
     @Override
     public String toString() {
         final StringBuilder builder = new StringBuilder();
diff --git a/core/store/hz/cluster/src/test/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStoreTest.java b/core/store/hz/cluster/src/test/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStoreTest.java
index 956b0be..5c867f4 100644
--- a/core/store/hz/cluster/src/test/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStoreTest.java
+++ b/core/store/hz/cluster/src/test/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStoreTest.java
@@ -218,7 +218,7 @@
             public void notify(MastershipEvent event) {
                 assertEquals("wrong event:", Type.MASTER_CHANGED, event.type());
                 assertEquals("wrong subject", DID1, event.subject());
-                assertEquals("wrong subject", N1, event.master());
+                assertEquals("wrong subject", N1, event.node());
                 addLatch.countDown();
             }
         };
diff --git a/core/store/hz/common/src/main/java/org/onlab/onos/store/common/RoleInfo.java b/core/store/hz/common/src/main/java/org/onlab/onos/store/common/RoleInfo.java
new file mode 100644
index 0000000..904488b
--- /dev/null
+++ b/core/store/hz/common/src/main/java/org/onlab/onos/store/common/RoleInfo.java
@@ -0,0 +1,69 @@
+package org.onlab.onos.store.common;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+
+import org.onlab.onos.cluster.NodeId;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A container for detailed role information for a device,
+ * within the current cluster. Role attributes include current
+ * master and a preference-ordered list of backup nodes.
+ */
+public class RoleInfo {
+    private final NodeId master;
+    private final List<NodeId> backups;
+
+    public RoleInfo(NodeId master, List<NodeId> backups) {
+        this.master = master;
+        this.backups = new LinkedList<>();
+
+        this.backups.addAll(checkNotNull(backups));
+    }
+
+    public NodeId master() {
+        return master;
+    }
+
+    public List<NodeId> backups() {
+        return Collections.unmodifiableList(backups);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == null) {
+            return false;
+        }
+        if (!(other instanceof RoleInfo)) {
+            return false;
+        }
+        RoleInfo that = (RoleInfo) other;
+        if (!Objects.equals(this.master, that.master)) {
+            return false;
+        }
+        if (!Objects.equals(this.backups, that.backups)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(master, backups.hashCode());
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("master: \n\t").append(master).append("\n");
+        builder.append("backups: \n");
+        for (NodeId n : backups) {
+            builder.append("\t").append(n).append("\n");
+        }
+        return builder.toString();
+    }
+}
diff --git a/core/store/hz/common/src/test/java/org/onlab/onos/store/common/RoleInfoTest.java b/core/store/hz/common/src/test/java/org/onlab/onos/store/common/RoleInfoTest.java
new file mode 100644
index 0000000..c7e43ff
--- /dev/null
+++ b/core/store/hz/common/src/test/java/org/onlab/onos/store/common/RoleInfoTest.java
@@ -0,0 +1,41 @@
+package org.onlab.onos.store.common;
+
+import java.util.List;
+
+import org.junit.Test;
+import org.onlab.onos.cluster.NodeId;
+
+import com.google.common.collect.Lists;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+/**
+ * Test to check behavioral correctness of the RoleInfo structure.
+ */
+public class RoleInfoTest {
+    private static final NodeId N1 = new NodeId("n1");
+    private static final NodeId N2 = new NodeId("n2");
+    private static final NodeId N3 = new NodeId("n3");
+    private static final NodeId N4 = new NodeId("n4");
+
+    private static final List<NodeId> BKUP1 = Lists.newArrayList(N2, N3);
+    private static final List<NodeId> BKUP2 = Lists.newArrayList(N3, N4);
+
+    private static final RoleInfo RI1 = new RoleInfo(N1, BKUP1);
+    private static final RoleInfo RI2 = new RoleInfo(N1, BKUP2);
+    private static final RoleInfo RI3 = new RoleInfo(N2, BKUP1);
+
+    @Test
+    public void basics() {
+        assertEquals("wrong master", new NodeId("n1"), RI1.master());
+        System.out.println(RI1.toString());
+        assertEquals("wrong Backups", RI1.backups(), Lists.newArrayList(N2, N3));
+
+        assertNotEquals("equals() broken", RI1, RI2);
+        assertNotEquals("equals() broken", RI1, RI3);
+
+        List<NodeId> bkup3 = Lists.newArrayList(N3, new NodeId("n4"));
+        assertEquals("equals() broken", new RoleInfo(N1, bkup3), RI2);
+    }
+}