ONOS-4326: Focusing on add/remove cluster member. (WIP).
If reviewing this, please refer to http://tinyurl.com/onos-ui-topo-model
Change-Id: Ic6568074ac768ec828f9103e92caab5e9a06ade6
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/ModelCache.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/ModelCache.java
index 6d5de44..420768f 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/ModelCache.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/ModelCache.java
@@ -17,6 +17,7 @@
package org.onosproject.ui.impl.topo.model;
import org.onosproject.cluster.ControllerNode;
+import org.onosproject.cluster.NodeId;
import org.onosproject.cluster.RoleInfo;
import org.onosproject.event.EventDispatcher;
import org.onosproject.net.Device;
@@ -24,11 +25,16 @@
import org.onosproject.net.Host;
import org.onosproject.net.Link;
import org.onosproject.net.region.Region;
+import org.onosproject.ui.model.ServiceBundle;
import org.onosproject.ui.model.topo.UiClusterMember;
import org.onosproject.ui.model.topo.UiDevice;
import org.onosproject.ui.model.topo.UiTopology;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.DEVICE_ADDED;
+import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.CLUSTER_MEMBER_ADDED_OR_UPDATED;
+import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.CLUSTER_MEMBER_REMOVED;
+import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.DEVICE_ADDED_OR_UPDATED;
import static org.onosproject.ui.impl.topo.model.UiModelEvent.Type.DEVICE_REMOVED;
/**
@@ -36,10 +42,14 @@
*/
class ModelCache {
+ private static final Logger log = LoggerFactory.getLogger(ModelCache.class);
+
+ private final ServiceBundle services;
private final EventDispatcher dispatcher;
private final UiTopology uiTopology = new UiTopology();
- ModelCache(EventDispatcher eventDispatcher) {
+ ModelCache(ServiceBundle services, EventDispatcher eventDispatcher) {
+ this.services = services;
this.dispatcher = eventDispatcher;
}
@@ -59,6 +69,7 @@
* Create our internal model of the global topology.
*/
void load() {
+ // TODO - implement loading of initial state
// loadClusterMembers();
// loadRegions();
// loadDevices();
@@ -74,20 +85,36 @@
* @param cnode controller node to be added/updated
*/
void addOrUpdateClusterMember(ControllerNode cnode) {
- UiClusterMember member = uiTopology.findClusterMember(cnode.id());
- if (member != null) {
- member.update(cnode);
- } else {
+ NodeId id = cnode.id();
+ UiClusterMember member = uiTopology.findClusterMember(id);
+ if (member == null) {
member = new UiClusterMember(cnode);
uiTopology.add(member);
}
- // TODO: post event
+ // inject computed data about the cluster node, into the model object
+ ControllerNode.State state = services.cluster().getState(id);
+ member.setState(state);
+ member.setDeviceCount(services.mastership().getDevicesOf(id).size());
+ // NOTE: UI-attached is session-based data, not global
+
+ dispatcher.post(new UiModelEvent(CLUSTER_MEMBER_ADDED_OR_UPDATED, member));
}
+ /**
+ * Removes from the model the specified controller node.
+ *
+ * @param cnode controller node to be removed
+ */
void removeClusterMember(ControllerNode cnode) {
- // TODO: find cluster member assoc. with parameter; remove from model
- // TODO: post event
+ NodeId id = cnode.id();
+ UiClusterMember member = uiTopology.findClusterMember(id);
+ if (member != null) {
+ uiTopology.remove(member);
+ dispatcher.post(new UiModelEvent(CLUSTER_MEMBER_REMOVED, member));
+ } else {
+ log.warn("Tried to remove non-member cluster node {}", id);
+ }
}
void updateMasterships(DeviceId deviceId, RoleInfo roleInfo) {
@@ -111,7 +138,7 @@
UiDevice uiDevice = new UiDevice();
// TODO: post the (correct) event
- dispatcher.post(new UiModelEvent(DEVICE_ADDED, uiDevice));
+ dispatcher.post(new UiModelEvent(DEVICE_ADDED_OR_UPDATED, uiDevice));
}
void removeDevice(Device device) {
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java
index c37f27d..1a2c66c 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiModelEvent.java
@@ -29,8 +29,12 @@
}
enum Type {
- DEVICE_ADDED,
+ CLUSTER_MEMBER_ADDED_OR_UPDATED,
+ CLUSTER_MEMBER_REMOVED,
+
+ DEVICE_ADDED_OR_UPDATED,
DEVICE_REMOVED,
+
// TODO...
}
}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java
index 0a4ce99..48ae42f 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java
@@ -59,6 +59,7 @@
import org.onosproject.net.statistic.StatisticService;
import org.onosproject.net.topology.TopologyService;
import org.onosproject.ui.impl.topo.UiTopoSession;
+import org.onosproject.ui.model.ServiceBundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -122,7 +123,7 @@
@Activate
protected void activate() {
- cache = new ModelCache(eventDispatcher);
+ cache = new ModelCache(new DefaultServiceBundle(), eventDispatcher);
eventDispatcher.addSink(UiModelEvent.class, listenerRegistry);
@@ -180,6 +181,52 @@
removeListener(session);
}
+ /**
+ * Default implementation of service bundle to return references to our
+ * dynamically injected services.
+ */
+ private class DefaultServiceBundle implements ServiceBundle {
+ @Override
+ public ClusterService cluster() {
+ return clusterService;
+ }
+
+ @Override
+ public MastershipService mastership() {
+ return mastershipService;
+ }
+
+ @Override
+ public RegionService region() {
+ return regionService;
+ }
+
+ @Override
+ public DeviceService device() {
+ return deviceService;
+ }
+
+ @Override
+ public LinkService link() {
+ return linkService;
+ }
+
+ @Override
+ public HostService host() {
+ return hostService;
+ }
+
+ @Override
+ public IntentService intent() {
+ return intentService;
+ }
+
+ @Override
+ public FlowRuleService flow() {
+ return flowService;
+ }
+ }
+
private class InternalClusterListener implements ClusterEventListener {
@Override
diff --git a/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/AbstractModelTest.java b/web/gui/src/test/java/org/onosproject/ui/impl/AbstractUiImplTest.java
similarity index 76%
rename from web/gui/src/test/java/org/onosproject/ui/impl/topo/model/AbstractModelTest.java
rename to web/gui/src/test/java/org/onosproject/ui/impl/AbstractUiImplTest.java
index 8cc047d..80bdd88 100644
--- a/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/AbstractModelTest.java
+++ b/web/gui/src/test/java/org/onosproject/ui/impl/AbstractUiImplTest.java
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-package org.onosproject.ui.impl.topo.model;
+package org.onosproject.ui.impl;
/**
- * Base class for model test classes.
+ * Base class for unit tests.
*/
-public abstract class AbstractModelTest {
+public class AbstractUiImplTest {
/**
* System agnostic end-of-line character.
@@ -41,7 +41,11 @@
* @param o object to print
*/
protected void print(Object o) {
- print(o.toString());
+ if (o == null) {
+ print("<null>");
+ } else {
+ print(o.toString());
+ }
}
/**
@@ -55,4 +59,12 @@
print(String.format(fmt, params));
}
+ /**
+ * Prints a title, to delimit individual unit test output.
+ *
+ * @param s a title for the test
+ */
+ protected void title(String s) {
+ print(EOL + "=== %s ===", s);
+ }
}
diff --git a/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/AbstractTopoModelTest.java b/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/AbstractTopoModelTest.java
new file mode 100644
index 0000000..6fbb443
--- /dev/null
+++ b/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/AbstractTopoModelTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2016-present 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.ui.impl.topo.model;
+
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.ClusterServiceAdapter;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.mastership.MastershipServiceAdapter;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.intent.IntentService;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.region.RegionService;
+import org.onosproject.ui.impl.AbstractUiImplTest;
+import org.onosproject.ui.model.ServiceBundle;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.onosproject.net.DeviceId.deviceId;
+
+/**
+ * Base class for model test classes.
+ */
+abstract class AbstractTopoModelTest extends AbstractUiImplTest {
+
+ /**
+ * Returns canned results.
+ * At some future point, we may make this "programmable", so that
+ * it returns certain values based on element IDs etc.
+ */
+ protected static final ServiceBundle MOCK_SERVICES =
+ new ServiceBundle() {
+ @Override
+ public ClusterService cluster() {
+ return MOCK_CLUSTER;
+ }
+
+ @Override
+ public MastershipService mastership() {
+ return MOCK_MASTER;
+ }
+
+ @Override
+ public RegionService region() {
+ return null;
+ }
+
+ @Override
+ public DeviceService device() {
+ return null;
+ }
+
+ @Override
+ public LinkService link() {
+ return null;
+ }
+
+ @Override
+ public HostService host() {
+ return null;
+ }
+
+ @Override
+ public IntentService intent() {
+ return null;
+ }
+
+ @Override
+ public FlowRuleService flow() {
+ return null;
+ }
+ };
+
+ private static final ClusterService MOCK_CLUSTER = new MockClusterService();
+ private static final MastershipService MOCK_MASTER = new MockMasterService();
+ // TODO: fill out as necessary
+
+ /*
+ Our mock environment:
+
+ Three controllers: C1, C2, C3
+
+ Nine devices: D1 .. D9
+
+ D4 ---+ +--- D7
+ | |
+ D5 --- D1 --- D2 --- D3 --- D8
+ | |
+ D6 ---+ +--- D9
+
+ Twelve hosts (two per D4 ... D9) H41, H42, H51, H52, ...
+
+ Regions:
+ R1 : D1, D2, D3
+ R2 : D4, D5, D6
+ R3 : D7, D8, D9
+
+ Mastership:
+ C1 : D1, D2, D3
+ C2 : D4, D5, D6
+ C3 : D7, D8, D9
+ */
+
+
+ private static class MockClusterService extends ClusterServiceAdapter {
+ private final Map<NodeId, ControllerNode.State> states = new HashMap<>();
+
+
+ @Override
+ public ControllerNode.State getState(NodeId nodeId) {
+ // For now, a hardcoded state of ACTIVE (but not READY)
+ // irrespective of the node ID.
+ return ControllerNode.State.ACTIVE;
+ }
+ }
+
+ protected static final DeviceId D1_ID = deviceId("D1");
+ protected static final DeviceId D2_ID = deviceId("D2");
+ protected static final DeviceId D3_ID = deviceId("D3");
+ protected static final DeviceId D4_ID = deviceId("D4");
+ protected static final DeviceId D5_ID = deviceId("D5");
+ protected static final DeviceId D6_ID = deviceId("D6");
+ protected static final DeviceId D7_ID = deviceId("D7");
+ protected static final DeviceId D8_ID = deviceId("D8");
+ protected static final DeviceId D9_ID = deviceId("D9");
+
+ private static class MockMasterService extends MastershipServiceAdapter {
+ @Override
+ public Set<DeviceId> getDevicesOf(NodeId nodeId) {
+ // For now, a hard coded set of two device IDs
+ // irrespective of the node ID.
+ return ImmutableSet.of(D1_ID, D2_ID);
+ }
+ }
+
+}
diff --git a/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java b/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java
index 23a2b1b..c272087 100644
--- a/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java
+++ b/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java
@@ -16,28 +16,113 @@
package org.onosproject.ui.impl.topo.model;
+import org.junit.Before;
import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.cluster.DefaultControllerNode;
+import org.onosproject.event.Event;
import org.onosproject.event.EventDispatcher;
+import org.onosproject.ui.impl.topo.model.UiModelEvent.Type;
+import org.onosproject.ui.model.topo.UiElement;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.onosproject.cluster.NodeId.nodeId;
/**
* Unit tests for {@link ModelCache}.
*/
-public class ModelCacheTest extends AbstractModelTest {
+public class ModelCacheTest extends AbstractTopoModelTest {
- private static final EventDispatcher DISPATCHER = event -> {
- // Do we care?
- };
+ private class TestEvDisp implements EventDispatcher {
+
+ private Event<Type, UiElement> lastEvent = null;
+ private int eventCount = 0;
+
+ @Override
+ public void post(Event event) {
+ lastEvent = event;
+ eventCount++;
+// print("Event dispatched: %s", event);
+ }
+
+ private void assertEventCount(int exp) {
+ assertEquals("unex event count", exp, eventCount);
+ }
+
+ private void assertLast(Type expEventType, String expId) {
+ assertNotNull("no last event", lastEvent);
+ assertEquals("unex event type", expEventType, lastEvent.type());
+ assertEquals("unex element ID", expId, lastEvent.subject().idAsString());
+ }
+ }
+
+ private static IpAddress ip(String s) {
+ return IpAddress.valueOf(s);
+ }
+
+ private static ControllerNode cnode(String id, String ip) {
+ return new DefaultControllerNode(nodeId(id), ip(ip));
+ }
+
+ private static final String C1 = "C1";
+ private static final String C2 = "C2";
+ private static final String C3 = "C3";
+
+ private static final ControllerNode NODE_1 = cnode(C1, "10.0.0.1");
+ private static final ControllerNode NODE_2 = cnode(C2, "10.0.0.2");
+ private static final ControllerNode NODE_3 = cnode(C3, "10.0.0.3");
+
+
+ private final TestEvDisp dispatcher = new TestEvDisp();
private ModelCache cache;
+ @Before
+ public void setUp() {
+ cache = new ModelCache(MOCK_SERVICES, dispatcher);
+ }
+
@Test
public void basic() {
- cache = new ModelCache(DISPATCHER);
+ title("basic");
print(cache);
assertEquals("unex # members", 0, cache.clusterMemberCount());
assertEquals("unex # regions", 0, cache.regionCount());
}
+ @Test
+ public void addAndRemoveClusterMember() {
+ title("addAndRemoveClusterMember");
+ print(cache);
+ assertEquals("unex # members", 0, cache.clusterMemberCount());
+ dispatcher.assertEventCount(0);
+
+ cache.addOrUpdateClusterMember(NODE_1);
+ print(cache);
+ assertEquals("unex # members", 1, cache.clusterMemberCount());
+ dispatcher.assertEventCount(1);
+ dispatcher.assertLast(Type.CLUSTER_MEMBER_ADDED_OR_UPDATED, C1);
+
+ cache.removeClusterMember(NODE_1);
+ print(cache);
+ assertEquals("unex # members", 0, cache.clusterMemberCount());
+ dispatcher.assertEventCount(2);
+ dispatcher.assertLast(Type.CLUSTER_MEMBER_REMOVED, C1);
+ }
+
+ @Test
+ public void createThreeNodeCluster() {
+ title("createThreeNodeCluster");
+ cache.addOrUpdateClusterMember(NODE_1);
+ dispatcher.assertLast(Type.CLUSTER_MEMBER_ADDED_OR_UPDATED, C1);
+ cache.addOrUpdateClusterMember(NODE_2);
+ dispatcher.assertLast(Type.CLUSTER_MEMBER_ADDED_OR_UPDATED, C2);
+ cache.addOrUpdateClusterMember(NODE_3);
+ dispatcher.assertLast(Type.CLUSTER_MEMBER_ADDED_OR_UPDATED, C3);
+ dispatcher.assertEventCount(3);
+ print(cache);
+ }
+
}