Fixing issues on GUI server side. Adding command to balance mastership. Messing with color scheme per feedback.

Change-Id: I89fb52105f7e724167a417e033048e9c88f31eae
diff --git a/cli/src/main/java/org/onlab/onos/cli/BalanceMastersCommand.java b/cli/src/main/java/org/onlab/onos/cli/BalanceMastersCommand.java
new file mode 100644
index 0000000..1f4c17d
--- /dev/null
+++ b/cli/src/main/java/org/onlab/onos/cli/BalanceMastersCommand.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.onos.cli;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.onos.cluster.ClusterService;
+import org.onlab.onos.cluster.ControllerNode;
+import org.onlab.onos.mastership.MastershipAdminService;
+import org.onlab.onos.mastership.MastershipService;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.MastershipRole;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+/**
+ * Forces device mastership rebalancing.
+ */
+@Command(scope = "onos", name = "balance-masters",
+        description = "Forces device mastership rebalancing")
+public class BalanceMastersCommand extends AbstractShellCommand {
+
+    @Override
+    protected void execute() {
+        ClusterService service = get(ClusterService.class);
+        MastershipService mastershipService = get(MastershipService.class);
+        MastershipAdminService adminService = get(MastershipAdminService.class);
+
+        List<ControllerNode> nodes = newArrayList(service.getNodes());
+
+        Multimap<ControllerNode, DeviceId> controllerDevices = HashMultimap.create();
+
+        // Create buckets reflecting current ownership.
+        for (ControllerNode node : nodes) {
+            controllerDevices.putAll(node, mastershipService.getDevicesOf(node.id()));
+        }
+
+        int bucketCount = nodes.size();
+        for (int i = 0; i < bucketCount / 2; i++) {
+            // Iterate over the buckets and find the smallest and the largest.
+            ControllerNode smallest = findSmallestBucket(controllerDevices);
+            ControllerNode largest = findLargestBucket(controllerDevices);
+            balanceBuckets(smallest, largest, controllerDevices,
+                           mastershipService, adminService);
+        }
+    }
+
+    private ControllerNode findSmallestBucket(Multimap<ControllerNode, DeviceId> controllerDevices) {
+        int minSize = Integer.MAX_VALUE;
+        ControllerNode minNode = null;
+        for (ControllerNode node : controllerDevices.keySet()) {
+            int size = controllerDevices.get(node).size();
+            if (size < minSize) {
+                minSize = size;
+                minNode = node;
+            }
+        }
+        return minNode;
+    }
+
+    private ControllerNode findLargestBucket(Multimap<ControllerNode, DeviceId> controllerDevices) {
+        int maxSize = -1;
+        ControllerNode maxNode = null;
+        for (ControllerNode node : controllerDevices.keySet()) {
+            int size = controllerDevices.get(node).size();
+            if (size >= maxSize) {
+                maxSize = size;
+                maxNode = node;
+            }
+        }
+        return maxNode;
+    }
+
+    // FIXME: enhance to better handle cases where smallest cannot take any of the devices from largest
+
+    private void balanceBuckets(ControllerNode smallest, ControllerNode largest,
+                                Multimap<ControllerNode, DeviceId> controllerDevices,
+                                MastershipService mastershipService,
+                                MastershipAdminService adminService) {
+        Collection<DeviceId> minBucket = controllerDevices.get(smallest);
+        Collection<DeviceId> maxBucket = controllerDevices.get(largest);
+
+        int delta = (maxBucket.size() - minBucket.size()) / 2;
+
+        print("Attempting to move %d nodes from %s to %s...",
+              delta, largest.id(), smallest.id());
+
+        int i = 0;
+        Iterator<DeviceId> it = maxBucket.iterator();
+        while (it.hasNext() && i < delta) {
+            DeviceId deviceId = it.next();
+
+            // Check that the transfer can happen for the current element.
+            if (mastershipService.getNodesFor(deviceId).backups().contains(smallest)) {
+                print("Setting %s as the new master for %s", smallest.id(), deviceId);
+                adminService.setRole(smallest.id(), deviceId, MastershipRole.MASTER);
+                i++;
+            }
+        }
+
+        controllerDevices.removeAll(smallest);
+    }
+
+}
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 7f55975..71ce11a 100644
--- a/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
+++ b/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -61,6 +61,9 @@
             <action class="org.onlab.onos.cli.MastersListCommand"/>
         </command>
         <command>
+            <action class="org.onlab.onos.cli.BalanceMastersCommand"/>
+        </command>
+        <command>
             <action class="org.onlab.onos.cli.ApplicationIdListCommand"/>
         </command>
 
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java
index f210b1d..7bb9b86 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewMessages.java
@@ -289,7 +289,7 @@
     }
 
     // Produces a cluster instance message to the client.
-    protected ObjectNode instanceMessage(ClusterEvent event) {
+    protected ObjectNode instanceMessage(ClusterEvent event, String messageType) {
         ControllerNode node = event.subject();
         int switchCount = mastershipService.getDevicesOf(node.id()).size();
         ObjectNode payload = mapper.createObjectNode()
@@ -307,8 +307,10 @@
         payload.set("labels", labels);
         addMetaUi(node.id().toString(), payload);
 
-        String type = (event.type() == INSTANCE_ADDED) ? "addInstance" :
-                ((event.type() == INSTANCE_REMOVED) ? "removeInstance" : "updateInstance");
+        String type = messageType != null ? messageType :
+                ((event.type() == INSTANCE_ADDED) ? "addInstance" :
+                        ((event.type() == INSTANCE_REMOVED ? "removeInstance" :
+                                "updateInstance")));
         return envelope(type, 0, payload);
     }
 
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java
index f65232e..57781de 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyViewWebSocket.java
@@ -24,6 +24,8 @@
 import org.onlab.onos.cluster.ControllerNode;
 import org.onlab.onos.core.ApplicationId;
 import org.onlab.onos.core.CoreService;
+import org.onlab.onos.mastership.MastershipEvent;
+import org.onlab.onos.mastership.MastershipListener;
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.Host;
@@ -46,6 +48,7 @@
 import org.onlab.onos.net.link.LinkEvent;
 import org.onlab.onos.net.link.LinkListener;
 import org.onlab.osgi.ServiceDirectory;
+import org.onlab.packet.Ethernet;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -62,6 +65,7 @@
 import static org.onlab.onos.net.DeviceId.deviceId;
 import static org.onlab.onos.net.HostId.hostId;
 import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_ADDED;
+import static org.onlab.onos.net.device.DeviceEvent.Type.DEVICE_UPDATED;
 import static org.onlab.onos.net.host.HostEvent.Type.HOST_ADDED;
 import static org.onlab.onos.net.link.LinkEvent.Type.LINK_ADDED;
 
@@ -80,8 +84,8 @@
 
     private static final String APP_ID = "org.onlab.onos.gui";
 
-    private static final long SUMMARY_FREQUENCY_SEC = 2000;
-    private static final long TRAFFIC_FREQUENCY_SEC = 1000;
+    private static final long SUMMARY_FREQUENCY_SEC = 3000;
+    private static final long TRAFFIC_FREQUENCY_SEC = 1500;
 
     private static final Comparator<? super ControllerNode> NODE_COMPARATOR =
             new Comparator<ControllerNode>() {
@@ -97,6 +101,7 @@
     private FrameConnection control;
 
     private final ClusterEventListener clusterListener = new InternalClusterListener();
+    private final MastershipListener mastershipListener = new InternalMastershipListener();
     private final DeviceListener deviceListener = new InternalDeviceListener();
     private final LinkListener linkListener = new InternalLinkListener();
     private final HostListener hostListener = new InternalHostListener();
@@ -164,7 +169,7 @@
         this.control = (FrameConnection) connection;
         addListeners();
 
-        sendAllInstances();
+        sendAllInstances(null);
         sendAllDevices();
         sendAllLinks();
         sendAllHosts();
@@ -235,11 +240,12 @@
     }
 
     // Sends all controller nodes to the client as node-added messages.
-    private void sendAllInstances() {
+    private void sendAllInstances(String messageType) {
         List<ControllerNode> nodes = new ArrayList<>(clusterService.getNodes());
         Collections.sort(nodes, NODE_COMPARATOR);
         for (ControllerNode node : nodes) {
-            sendMessage(instanceMessage(new ClusterEvent(INSTANCE_ADDED, node)));
+            sendMessage(instanceMessage(new ClusterEvent(INSTANCE_ADDED, node),
+                                        messageType));
         }
     }
 
@@ -307,13 +313,14 @@
 
         // FIXME: clearly, this is not enough
         TrafficSelector selector = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_IPV4)
                 .matchEthDst(dstHost.mac()).build();
         TrafficTreatment treatment = DefaultTrafficTreatment.builder().build();
 
         MultiPointToSinglePointIntent intent =
                 new MultiPointToSinglePointIntent(appId, selector, treatment,
                                                   ingressPoints, dstHost.location());
-        trafficEvent = event;
+        startMonitoring(event);
         intentService.submit(intent);
     }
 
@@ -359,7 +366,6 @@
 
     // Subscribes for host traffic messages.
     private synchronized void requestAllTraffic(ObjectNode event) {
-        ObjectNode payload = payload(event);
         long sid = startMonitoring(event);
         sendMessage(trafficSummaryMessage(sid));
     }
@@ -375,7 +381,6 @@
 
         // If there is a hover node, include it in the hosts and find intents.
         String hover = string(payload, "hover");
-        Set<Intent> hoverIntents;
         if (!isNullOrEmpty(hover)) {
             addHover(hosts, devices, hover);
         }
@@ -447,6 +452,7 @@
     // Adds all internal listeners.
     private void addListeners() {
         clusterService.addListener(clusterListener);
+        mastershipService.addListener(mastershipListener);
         deviceService.addListener(deviceListener);
         linkService.addListener(linkListener);
         hostService.addListener(hostListener);
@@ -458,6 +464,7 @@
         if (!listenersRemoved) {
             listenersRemoved = true;
             clusterService.removeListener(clusterListener);
+            mastershipService.removeListener(mastershipListener);
             deviceService.removeListener(deviceListener);
             linkService.removeListener(linkListener);
             hostService.removeListener(hostListener);
@@ -469,7 +476,17 @@
     private class InternalClusterListener implements ClusterEventListener {
         @Override
         public void event(ClusterEvent event) {
-            sendMessage(instanceMessage(event));
+            sendMessage(instanceMessage(event, null));
+        }
+    }
+
+    // Mastership change listener
+    private class InternalMastershipListener implements MastershipListener {
+        @Override
+        public void event(MastershipEvent event) {
+            sendAllInstances("updateInstance");
+            Device device = deviceService.getDevice(event.subject());
+            sendMessage(deviceMessage(new DeviceEvent(DEVICE_UPDATED, device)));
         }
     }
 
diff --git a/web/gui/src/main/webapp/d3Utils.js b/web/gui/src/main/webapp/d3Utils.js
index 2e49950..beb7e09 100644
--- a/web/gui/src/main/webapp/d3Utils.js
+++ b/web/gui/src/main/webapp/d3Utils.js
@@ -123,10 +123,15 @@
     // TODO: tune colors for light and dark themes
 
     //                blue       purple     pink       mustard    cyan       green      red
-    var lightNorm = ['#1f77b4', '#9467bd', '#e377c2', '#bcbd22', '#17becf', '#2ca02c', '#d62728'],
-        lightMute = ['#aec7e8', '#c5b0d5', '#f7b6d2', '#dbdb8d', '#9edae5', '#98df8a', '#ff9896'],
-        darkNorm = ['#1f77b4', '#9467bd', '#e377c2', '#bcbd22', '#17becf', '#2ca02c', '#d62728'],
-        darkMute = ['#aec7e8', '#c5b0d5', '#f7b6d2', '#dbdb8d', '#9edae5', '#98df8a', '#ff9896'];
+    //var lightNorm = ['#1f77b4', '#9467bd', '#e377c2', '#bcbd22', '#17becf', '#2ca02c', '#d62728'],
+    //    lightMute = ['#aec7e8', '#c5b0d5', '#f7b6d2', '#dbdb8d', '#9edae5', '#98df8a', '#ff9896'],
+    //    darkNorm = ['#1f77b4', '#9467bd', '#e377c2', '#bcbd22', '#17becf', '#2ca02c', '#d62728'],
+    //    darkMute = ['#aec7e8', '#c5b0d5', '#f7b6d2', '#dbdb8d', '#9edae5', '#98df8a', '#ff9896'];
+
+    var lightNorm = ['#3F587F', '#77533D', '#C94E30', '#892D78', '#138C62', '#006D72', '#59AD00'],
+        lightMute = ['#56657C', '#665F57', '#C68C7F', '#876E82', '#68897E', '#4E6F70', '#93AA7B'],
+        darkNorm = ['#3F587F', '#77533D', '#C94E30', '#892D78', '#138C62', '#006D72', '#59AD00'],
+        darkMute = ['#56657C', '#665F57', '#C68C7F', '#876E82', '#68897E', '#4E6F70', '#93AA7B'];
 
     function cat7() {
         var colors = {
diff --git a/web/gui/src/main/webapp/mast2.css b/web/gui/src/main/webapp/mast2.css
index 4d108c9..4bf3d5d 100644
--- a/web/gui/src/main/webapp/mast2.css
+++ b/web/gui/src/main/webapp/mast2.css
@@ -51,7 +51,7 @@
     color: #369;
 }
 .dark #mast span.title {
-    color: #acf;
+    color: #eee;
 }
 
 #mast span.right {
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index cfefba9..ead2730 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -356,7 +356,7 @@
     fill: #888;
 }
 #topo-oibox .online svg text {
-    fill: #000;
+    fill: #eee;
 }
 
 #topo-oibox svg text.instTitle {
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index ba21295..87632dc 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -1088,8 +1088,8 @@
 
             svg.append('rect').attr(rectAttr);
 
-            appendGlyph(svg, c.nodeOx, c.nodeOy, c.nodeDim, '#node');
-            appendBadge(svg, c.birdOx, c.birdOy, c.birdDim, '#bird');
+            //appendGlyph(svg, c.nodeOx, c.nodeOy, c.nodeDim, '#node');
+            appendBadge(svg, 14, 14, 28, '#bird');
 
             if (d.uiAttached) {
                 attachUiBadge(svg);