Added ability to track whether or not node has all components running fully.

Change-Id: Ib2b90c7a842976a3b3a9711367fa1eed43103b17
diff --git a/cli/src/main/java/org/onosproject/cli/NodesListCommand.java b/cli/src/main/java/org/onosproject/cli/NodesListCommand.java
index 6e91738..1664265 100644
--- a/cli/src/main/java/org/onosproject/cli/NodesListCommand.java
+++ b/cli/src/main/java/org/onosproject/cli/NodesListCommand.java
@@ -22,7 +22,7 @@
 import org.apache.karaf.shell.commands.Command;
 import org.joda.time.DateTime;
 import org.onlab.util.Tools;
-import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.ClusterAdminService;
 import org.onosproject.cluster.ControllerNode;
 import org.onosproject.utils.Comparators;
 
@@ -36,15 +36,14 @@
  * Lists all controller cluster nodes.
  */
 @Command(scope = "onos", name = "nodes",
-         description = "Lists all controller cluster nodes")
+        description = "Lists all controller cluster nodes")
 public class NodesListCommand extends AbstractShellCommand {
 
-    private static final String FMT =
-            "id=%s, address=%s:%s, state=%s, updated=%s %s";
+    private static final String FMT = "id=%s, address=%s:%s, state=%s, updated=%s %s";
 
     @Override
     protected void execute() {
-        ClusterService service = get(ClusterService.class);
+        ClusterAdminService service = get(ClusterAdminService.class);
         List<ControllerNode> nodes = newArrayList(service.getNodes());
         Collections.sort(nodes, Comparators.NODE_COMPARATOR);
         if (outputJson()) {
@@ -58,26 +57,24 @@
                     timeAgo = Tools.timeAgo(lastUpdated.getMillis());
                 }
                 print(FMT, node.id(), node.ip(), node.tcpPort(),
-                      service.getState(node.id()),
-                      timeAgo,
+                      service.getState(node.id()), timeAgo,
                       node.equals(self) ? "*" : "");
             }
         }
     }
 
     // Produces JSON structure.
-    private JsonNode json(ClusterService service, List<ControllerNode> nodes) {
+    private JsonNode json(ClusterAdminService service, List<ControllerNode> nodes) {
         ObjectMapper mapper = new ObjectMapper();
         ArrayNode result = mapper.createArrayNode();
         ControllerNode self = service.getLocalNode();
         for (ControllerNode node : nodes) {
             ControllerNode.State nodeState = service.getState(node.id());
             ObjectNode newNode = mapper.createObjectNode()
-                               .put("id", node.id().toString())
-                               .put("ip", node.ip().toString())
-                               .put("tcpPort", node.tcpPort())
-                               .put("self", node.equals(self));
-
+                    .put("id", node.id().toString())
+                    .put("ip", node.ip().toString())
+                    .put("tcpPort", node.tcpPort())
+                    .put("self", node.equals(self));
             if (nodeState != null) {
                 newNode.put("state", nodeState.toString());
             }
diff --git a/cli/src/main/java/org/onosproject/cli/SummaryCommand.java b/cli/src/main/java/org/onosproject/cli/SummaryCommand.java
index 65f4cf9..748e755 100644
--- a/cli/src/main/java/org/onosproject/cli/SummaryCommand.java
+++ b/cli/src/main/java/org/onosproject/cli/SummaryCommand.java
@@ -49,7 +49,7 @@
         for (final ControllerNode node : nodes) {
             final ControllerNode.State nodeState =
                     get(ClusterService.class).getState(node.id());
-            if (nodeState == ControllerNode.State.ACTIVE) {
+            if (nodeState.isActive()) {
                 nodeCount++;
             }
         }
diff --git a/core/api/src/main/java/org/onosproject/cluster/ClusterAdminService.java b/core/api/src/main/java/org/onosproject/cluster/ClusterAdminService.java
index 4794487..84d4f92 100644
--- a/core/api/src/main/java/org/onosproject/cluster/ClusterAdminService.java
+++ b/core/api/src/main/java/org/onosproject/cluster/ClusterAdminService.java
@@ -22,7 +22,7 @@
 /**
  * Service for administering the cluster node membership.
  */
-public interface ClusterAdminService {
+public interface ClusterAdminService extends ClusterService {
 
     /**
      * Forms cluster configuration based on the specified set of node
@@ -50,4 +50,11 @@
      */
     void removeNode(NodeId nodeId);
 
+    /**
+     * Marks the current node as fully started or not.
+     *
+     * @param started true indicates all components have been started
+     */
+    void markFullyStarted(boolean started);
+
 }
diff --git a/core/api/src/main/java/org/onosproject/cluster/ClusterService.java b/core/api/src/main/java/org/onosproject/cluster/ClusterService.java
index 015a648..323b9c7 100644
--- a/core/api/src/main/java/org/onosproject/cluster/ClusterService.java
+++ b/core/api/src/main/java/org/onosproject/cluster/ClusterService.java
@@ -50,7 +50,9 @@
     ControllerNode getNode(NodeId nodeId);
 
     /**
-     * Returns the availability state of the specified controller node.
+     * Returns the availability state of the specified controller node. Note
+     * that this does not imply that all the core and application components
+     * have been fully activated; only that the node has joined the cluster.
      *
      * @param nodeId controller node identifier
      * @return availability state
diff --git a/core/api/src/main/java/org/onosproject/cluster/ClusterStore.java b/core/api/src/main/java/org/onosproject/cluster/ClusterStore.java
index 0481d51..64348a3 100644
--- a/core/api/src/main/java/org/onosproject/cluster/ClusterStore.java
+++ b/core/api/src/main/java/org/onosproject/cluster/ClusterStore.java
@@ -57,6 +57,13 @@
     ControllerNode.State getState(NodeId nodeId);
 
     /**
+     * Marks the current node as fully started.
+     *
+     * @param started true indicates all components have been started
+     */
+    void markFullyStarted(boolean started);
+
+    /**
      * Returns the system when the availability state was last updated.
      *
      * @param nodeId controller node identifier
diff --git a/core/api/src/main/java/org/onosproject/cluster/ControllerNode.java b/core/api/src/main/java/org/onosproject/cluster/ControllerNode.java
index 2f74ae6..2c33c99 100644
--- a/core/api/src/main/java/org/onosproject/cluster/ControllerNode.java
+++ b/core/api/src/main/java/org/onosproject/cluster/ControllerNode.java
@@ -25,6 +25,12 @@
     /** Represents the operational state of the instance. */
     enum State {
         /**
+         * Signifies that the instance is active and that all components are
+         * operating normally.
+         */
+        READY,
+
+        /**
          * Signifies that the instance is active and operating normally.
          */
         ACTIVE,
@@ -33,7 +39,25 @@
          * Signifies that the instance is inactive, which means either down or
          * up, but not operational.
          */
-        INACTIVE
+        INACTIVE;
+
+        /**
+         * Indicates whether the state represents node which is active or ready.
+         *
+         * @return true if active or ready
+         */
+        public boolean isActive() {
+            return this == ACTIVE || this == READY;
+        }
+
+        /**
+         * Indicates whether the state represents a node which is ready.
+         *
+         * @return true if active and ready
+         */
+        public boolean isReady() {
+            return this == READY;
+        }
     }
 
     /**
diff --git a/core/common/src/test/java/org/onosproject/store/trivial/SimpleClusterStore.java b/core/common/src/test/java/org/onosproject/store/trivial/SimpleClusterStore.java
index 1a2799e..093020a 100644
--- a/core/common/src/test/java/org/onosproject/store/trivial/SimpleClusterStore.java
+++ b/core/common/src/test/java/org/onosproject/store/trivial/SimpleClusterStore.java
@@ -67,6 +67,7 @@
     protected EventDeliveryService eventDispatcher;
 
     private ListenerRegistry<IntentPartitionEvent, IntentPartitionEventListener> listenerRegistry;
+    private boolean started = false;
 
     @Activate
     public void activate() {
@@ -106,6 +107,11 @@
     }
 
     @Override
+    public void markFullyStarted(boolean started) {
+        this.started = started;
+    }
+
+    @Override
     public DateTime getLastUpdated(NodeId nodeId) {
         return creationTime;
     }
diff --git a/core/net/pom.xml b/core/net/pom.xml
index 9f611c7..026a34d 100644
--- a/core/net/pom.xml
+++ b/core/net/pom.xml
@@ -80,6 +80,11 @@
         </dependency>
 
         <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+
+        <dependency>
             <groupId>org.onosproject</groupId>
             <artifactId>onos-incubator-api</artifactId>
         </dependency>
@@ -93,6 +98,11 @@
             <groupId>org.apache.karaf.system</groupId>
             <artifactId>org.apache.karaf.system.core</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr</artifactId>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/core/net/src/main/java/org/onosproject/cluster/impl/ClusterManager.java b/core/net/src/main/java/org/onosproject/cluster/impl/ClusterManager.java
index 904fdff..7c70b8a 100644
--- a/core/net/src/main/java/org/onosproject/cluster/impl/ClusterManager.java
+++ b/core/net/src/main/java/org/onosproject/cluster/impl/ClusterManager.java
@@ -15,6 +15,8 @@
  */
 package org.onosproject.cluster.impl;
 
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Sets;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -44,9 +46,6 @@
 import org.onosproject.event.AbstractListenerManager;
 import org.slf4j.Logger;
 
-import com.google.common.collect.Collections2;
-import com.google.common.collect.Sets;
-
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -58,8 +57,8 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.onosproject.security.AppGuard.checkPermission;
+import static org.onosproject.security.AppPermission.Type.CLUSTER_READ;
 import static org.slf4j.LoggerFactory.getLogger;
-import static org.onosproject.security.AppPermission.Type.*;
 
 /**
  * Implementation of the cluster service.
@@ -133,6 +132,10 @@
         return store.getState(nodeId);
     }
 
+    @Override
+    public void markFullyStarted(boolean started) {
+        store.markFullyStarted(started);
+    }
 
     @Override
     public DateTime getLastUpdated(NodeId nodeId) {
diff --git a/core/net/src/main/java/org/onosproject/cluster/impl/ComponentsMonitor.java b/core/net/src/main/java/org/onosproject/cluster/impl/ComponentsMonitor.java
new file mode 100644
index 0000000..5d670a3
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/cluster/impl/ComponentsMonitor.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2016 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.onosproject.cluster.impl;
+
+import org.apache.felix.scr.Component;
+import org.apache.felix.scr.ScrService;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.FeaturesService;
+import org.onlab.util.SharedScheduledExecutors;
+import org.onosproject.cluster.ClusterAdminService;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Monitors the system to make sure that all bundles and their components
+ * are properly activated and keeps the cluster node service appropriately
+ * updated.
+ */
+@org.apache.felix.scr.annotations.Component(immediate = true)
+public class ComponentsMonitor {
+
+    private Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final long PERIOD = 2500;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected FeaturesService featuresService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ScrService scrService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterAdminService clusterAdminService;
+
+    private BundleContext bundleContext;
+    private ScheduledFuture<?> poller;
+
+    @Activate
+    protected void activate(ComponentContext context) {
+        bundleContext = context.getBundleContext();
+        poller = SharedScheduledExecutors.getSingleThreadExecutor()
+                .scheduleAtFixedRate(this::checkStartedState, PERIOD,
+                                     PERIOD, TimeUnit.MILLISECONDS);
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        poller.cancel(false);
+        log.info("Stopped");
+    }
+
+    private void checkStartedState() {
+        clusterAdminService.markFullyStarted(isFullyStarted());
+    }
+
+    /**
+     * Scans the system to make sure that all bundles and their components
+     * are fully started.
+     *
+     * @return true if all bundles and their components are active
+     */
+    private boolean isFullyStarted() {
+        for (Feature feature : featuresService.listInstalledFeatures()) {
+            if (!isFullyStarted(feature)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean isFullyStarted(Feature feature) {
+        return feature.getBundles().stream()
+                .map(info -> bundleContext.getBundle(info.getLocation()))
+                .allMatch(this::isFullyStarted);
+    }
+
+    private boolean isFullyStarted(Bundle bundle) {
+        Component[] components = scrService.getComponents(bundle);
+        if (components != null) {
+            for (Component component : components) {
+                if (!isFullyStarted(component)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    private boolean isFullyStarted(Component component) {
+        int state = component.getState();
+        return state == Component.STATE_ACTIVE || state == Component.STATE_DISABLED ||
+                (state == Component.STATE_REGISTERED && !component.isImmediate());
+    }
+
+}
diff --git a/core/net/src/main/java/org/onosproject/cluster/impl/LeadershipManager.java b/core/net/src/main/java/org/onosproject/cluster/impl/LeadershipManager.java
index e9a78d6..9ceffee 100644
--- a/core/net/src/main/java/org/onosproject/cluster/impl/LeadershipManager.java
+++ b/core/net/src/main/java/org/onosproject/cluster/impl/LeadershipManager.java
@@ -76,7 +76,7 @@
         deadlockDetector.scheduleWithFixedDelay(() -> clusterService.getNodes()
                 .stream()
                 .map(ControllerNode::id)
-                .filter(id -> clusterService.getState(id) != ControllerNode.State.ACTIVE)
+                .filter(id -> !clusterService.getState(id).isActive())
                 .forEach(this::unregister), 0, 2, TimeUnit.SECONDS);
         log.info("Started");
     }
diff --git a/core/net/src/main/java/org/onosproject/cluster/impl/MastershipManager.java b/core/net/src/main/java/org/onosproject/cluster/impl/MastershipManager.java
index 50a3350..e746d92 100644
--- a/core/net/src/main/java/org/onosproject/cluster/impl/MastershipManager.java
+++ b/core/net/src/main/java/org/onosproject/cluster/impl/MastershipManager.java
@@ -30,8 +30,8 @@
 import org.onosproject.cluster.ControllerNode;
 import org.onosproject.cluster.NodeId;
 import org.onosproject.cluster.RoleInfo;
-import org.onosproject.event.AbstractListenerManager;
 import org.onosproject.core.MetricsHelper;
+import org.onosproject.event.AbstractListenerManager;
 import org.onosproject.mastership.MastershipAdminService;
 import org.onosproject.mastership.MastershipEvent;
 import org.onosproject.mastership.MastershipListener;
@@ -57,11 +57,11 @@
 import static com.google.common.collect.Lists.newArrayList;
 import static org.onlab.metrics.MetricsUtil.startTimer;
 import static org.onlab.metrics.MetricsUtil.stopTimer;
-import static org.onosproject.cluster.ControllerNode.State.ACTIVE;
 import static org.onosproject.net.MastershipRole.MASTER;
 import static org.onosproject.security.AppGuard.checkPermission;
+import static org.onosproject.security.AppPermission.Type.CLUSTER_READ;
+import static org.onosproject.security.AppPermission.Type.CLUSTER_WRITE;
 import static org.slf4j.LoggerFactory.getLogger;
-import static org.onosproject.security.AppPermission.Type.*;
 
 
 
@@ -204,7 +204,7 @@
 
         // Create buckets reflecting current ownership.
         for (ControllerNode node : nodes) {
-            if (clusterService.getState(node.id()) == ACTIVE) {
+            if (clusterService.getState(node.id()).isActive()) {
                 Set<DeviceId> devicesOf = new HashSet<>(getDevicesOf(node.id()));
                 deviceCount += devicesOf.size();
                 controllerDevices.put(node, devicesOf);
diff --git a/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/DistributedClusterStore.java b/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/DistributedClusterStore.java
index b537517..5c34270 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/DistributedClusterStore.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/cluster/impl/DistributedClusterStore.java
@@ -18,7 +18,6 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
-
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -90,6 +89,7 @@
     private final Map<NodeId, ControllerNode> allNodes = Maps.newConcurrentMap();
     private final Map<NodeId, State> nodeStates = Maps.newConcurrentMap();
     private final Map<NodeId, DateTime> nodeStateLastUpdatedTimes = Maps.newConcurrentMap();
+
     private ScheduledExecutorService heartBeatSender = Executors.newSingleThreadScheduledExecutor(
             groupedThreads("onos/cluster/membership", "heartbeat-sender"));
     private ExecutorService heartBeatMessageHandler = Executors.newSingleThreadExecutor(
@@ -168,6 +168,11 @@
     }
 
     @Override
+    public void markFullyStarted(boolean started) {
+        updateState(localNode.id(), started ? State.READY : State.ACTIVE);
+    }
+
+    @Override
     public ControllerNode addNode(NodeId nodeId, IpAddress ip, int tcpPort) {
         ControllerNode node = new DefaultControllerNode(nodeId, ip, tcpPort);
         addNode(node);
@@ -201,13 +206,14 @@
                     .stream()
                     .filter(node -> !(node.id().equals(localNode.id())))
                     .collect(Collectors.toSet());
-            byte[] hbMessagePayload = SERIALIZER.encode(new HeartbeatMessage(localNode, peers));
+            State state = nodeStates.get(localNode.id());
+            byte[] hbMessagePayload = SERIALIZER.encode(new HeartbeatMessage(localNode, state, peers));
             peers.forEach((node) -> {
                 heartbeatToPeer(hbMessagePayload, node);
                 State currentState = nodeStates.get(node.id());
                 double phi = failureDetector.phi(node.id());
                 if (phi >= PHI_FAILURE_THRESHOLD) {
-                    if (currentState == State.ACTIVE) {
+                    if (currentState.isActive()) {
                         updateState(node.id(), State.INACTIVE);
                         notifyStateChange(node.id(), State.ACTIVE, State.INACTIVE);
                     }
@@ -225,7 +231,7 @@
 
     private void notifyStateChange(NodeId nodeId, State oldState, State newState) {
         ControllerNode node = allNodes.get(nodeId);
-        if (newState == State.ACTIVE) {
+        if (newState.isActive()) {
             notifyDelegate(new ClusterEvent(ClusterEvent.Type.INSTANCE_ACTIVATED, node));
         } else {
             notifyDelegate(new ClusterEvent(ClusterEvent.Type.INSTANCE_DEACTIVATED, node));
@@ -246,6 +252,7 @@
         public void accept(Endpoint sender, byte[] message) {
             HeartbeatMessage hb = SERIALIZER.decode(message);
             failureDetector.report(hb.source().id());
+            updateState(hb.source().id(), hb.state);
             hb.knownPeers().forEach(node -> {
                 allNodes.put(node.id(), node);
             });
@@ -254,10 +261,12 @@
 
     private static class HeartbeatMessage {
         private ControllerNode source;
+        private State state;
         private Set<ControllerNode> knownPeers;
 
-        public HeartbeatMessage(ControllerNode source, Set<ControllerNode> members) {
+        public HeartbeatMessage(ControllerNode source, State state, Set<ControllerNode> members) {
             this.source = source;
+            this.state = state != null ? state : State.ACTIVE;
             this.knownPeers = ImmutableSet.copyOf(members);
         }
 
diff --git a/core/store/dist/src/main/java/org/onosproject/store/intent/impl/IntentPartitionManager.java b/core/store/dist/src/main/java/org/onosproject/store/intent/impl/IntentPartitionManager.java
index f859ce4..0ab9704 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/intent/impl/IntentPartitionManager.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/intent/impl/IntentPartitionManager.java
@@ -22,7 +22,6 @@
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
 import org.onosproject.cluster.ClusterService;
-import org.onosproject.cluster.ControllerNode;
 import org.onosproject.cluster.Leadership;
 import org.onosproject.cluster.LeadershipEvent;
 import org.onosproject.cluster.LeadershipEventListener;
@@ -30,10 +29,10 @@
 import org.onosproject.cluster.NodeId;
 import org.onosproject.event.EventDeliveryService;
 import org.onosproject.event.ListenerRegistry;
-import org.onosproject.net.intent.Key;
 import org.onosproject.net.intent.IntentPartitionEvent;
 import org.onosproject.net.intent.IntentPartitionEventListener;
 import org.onosproject.net.intent.IntentPartitionService;
+import org.onosproject.net.intent.Key;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -173,7 +172,7 @@
     private void rebalance() {
         int activeNodes = (int) clusterService.getNodes()
                 .stream()
-                .filter(node -> ControllerNode.State.ACTIVE == clusterService.getState(node.id()))
+                .filter(node -> clusterService.getState(node.id()).isActive())
                 .count();
 
         int myShare = (int) Math.ceil((double) NUM_PARTITIONS / activeNodes);
diff --git a/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/EventuallyConsistentMapImpl.java b/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/EventuallyConsistentMapImpl.java
index fc7c0eb..4f70629 100644
--- a/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/EventuallyConsistentMapImpl.java
+++ b/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/EventuallyConsistentMapImpl.java
@@ -556,7 +556,7 @@
                 .stream()
                 .map(ControllerNode::id)
                 .filter(id -> !localNodeId.equals(id))
-                .filter(id -> clusterService.getState(id) == ControllerNode.State.ACTIVE)
+                .filter(id -> clusterService.getState(id).isActive())
                 .collect(Collectors.toList());
         Collections.shuffle(activePeers);
         return activePeers.isEmpty() ? Optional.empty() : Optional.of(activePeers.get(0));
diff --git a/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/MutexExecutionManager.java b/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/MutexExecutionManager.java
index 431a240..bc45d68 100644
--- a/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/MutexExecutionManager.java
+++ b/core/store/primitives/src/main/java/org/onosproject/store/primitives/impl/MutexExecutionManager.java
@@ -200,7 +200,7 @@
             long activeNodes = clusterService.getNodes()
                                              .stream()
                                              .map(node -> clusterService.getState(node.id()))
-                                             .filter(State.ACTIVE::equals)
+                                             .filter(State::isActive)
                                              .count();
             if (clusterService.getNodes().size() > 1 && activeNodes == 1) {
                 log.info("This node is partitioned away from the cluster. Stopping all inflight executions");
diff --git a/pom.xml b/pom.xml
index c19cbb5..29e9711 100644
--- a/pom.xml
+++ b/pom.xml
@@ -262,6 +262,12 @@
                 <version>1.9.12</version>
                 <scope>provided</scope>
             </dependency>
+            <dependency>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>org.apache.felix.scr</artifactId>
+                <version>1.8.2</version>
+            </dependency>
+
 
             <dependency>
                 <groupId>org.apache.karaf.features</groupId>
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/ClusterViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/ClusterViewMessageHandler.java
index a7eaaee..ccc7626 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/ClusterViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/ClusterViewMessageHandler.java
@@ -44,10 +44,11 @@
     private static final String IP = "ip";
     private static final String TCP_PORT = "tcp";
     private static final String STATE_IID = "_iconid_state";
+    private static final String STARTED_IID = "_iconid_started";
     private static final String UPDATED = "updated";
 
     private static final String[] COL_IDS = {
-            ID, IP, TCP_PORT, STATE_IID, UPDATED
+            ID, IP, TCP_PORT, STATE_IID, STARTED_IID, UPDATED
     };
 
     private static final String ICON_ID_ONLINE = "active";
@@ -95,13 +96,15 @@
                                  ClusterService cs) {
             NodeId id = node.id();
             DateTime lastUpdated = cs.getLastUpdated(id);
-            String iconId = (cs.getState(id) == ControllerNode.State.ACTIVE) ?
-                    ICON_ID_ONLINE : ICON_ID_OFFLINE;
+            ControllerNode.State state = cs.getState(id);
+            String iconId = state.isActive() ? ICON_ID_ONLINE : ICON_ID_OFFLINE;
+            String startedId = state.isReady() ? ICON_ID_ONLINE : ICON_ID_OFFLINE;
 
             row.cell(ID, id)
                 .cell(IP, node.ip())
                 .cell(TCP_PORT, node.tcpPort())
                 .cell(STATE_IID, iconId)
+                .cell(STARTED_IID, startedId)
                 .cell(UPDATED, lastUpdated);
         }
     }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
index b700a54..e1b4bea 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
@@ -81,7 +81,6 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Strings.isNullOrEmpty;
-import static org.onosproject.cluster.ControllerNode.State.ACTIVE;
 import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
 import static org.onosproject.net.PortNumber.portNumber;
 import static org.onosproject.ui.topo.TopoConstants.CoreButtons;
@@ -230,7 +229,7 @@
         ObjectNode payload = objectNode()
                 .put("id", node.id().toString())
                 .put("ip", node.ip().toString())
-                .put("online", clusterService.getState(node.id()) == ACTIVE)
+                .put("online", clusterService.getState(node.id()).isActive())
                 .put("uiAttached", node.equals(clusterService.getLocalNode()))
                 .put("switches", switchCount);
 
diff --git a/web/gui/src/main/webapp/app/view/cluster/cluster.html b/web/gui/src/main/webapp/app/view/cluster/cluster.html
index 0923529..dd202a3 100644
--- a/web/gui/src/main/webapp/app/view/cluster/cluster.html
+++ b/web/gui/src/main/webapp/app/view/cluster/cluster.html
@@ -30,7 +30,8 @@
         <div class="table-header" onos-sortable-header>
             <table>
                 <tr>
-                    <td colId="_iconid_state" class="table-icon" sortable></td>
+                    <td colId="_iconid_state" class="table-icon" col-width="60px" sortable>Active </td>
+                    <td colId="_iconid_started" class="table-icon" col-width="60px" sortable>Started </td>
                     <td colId="id" sortable>ID </td>
                     <td colId="ip" sortable>IP Address </td>
                     <td colId="tcp" sortable>TCP Port </td>
@@ -52,6 +53,9 @@
                     <td class="table-icon">
                         <div icon icon-id="{{node._iconid_state}}"></div>
                     </td>
+                    <td class="table-icon">
+                        <div icon icon-id="{{node._iconid_started}}"></div>
+                    </td>
                     <td>{{node.id}}</td>
                     <td>{{node.ip}}</td>
                     <td>{{node.tcp}}</td>