Simon Hunt

ONOS-4326: TopoRegions: Implement basic structure of response to 'topo2Start' event.

- this is WIP: still need to extract data from model cache.

Change-Id: I5ab843a1c352275a8da89964c886b660e3b8b616
......@@ -19,7 +19,7 @@ package org.onosproject.ui.model.topo;
/**
* Represents a node drawn on the topology view (region, device, host).
*/
abstract class UiNode extends UiElement {
public abstract class UiNode extends UiElement {
/**
* Default "layer" tag.
......
......@@ -37,12 +37,19 @@ import org.onosproject.ui.model.topo.UiClusterMember;
import org.onosproject.ui.model.topo.UiDevice;
import org.onosproject.ui.model.topo.UiHost;
import org.onosproject.ui.model.topo.UiLink;
import org.onosproject.ui.model.topo.UiNode;
import org.onosproject.ui.model.topo.UiRegion;
import org.onosproject.ui.model.topo.UiTopoLayout;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
/**
* Facility for creating JSON messages to send to the topology view in the
......@@ -50,6 +57,11 @@ import static com.google.common.base.Preconditions.checkNotNull;
*/
class Topo2Jsonifier {
private static final String E_DEF_NOT_LAST =
"UiNode.LAYER_DEFAULT not last in layer list";
private static final String E_UNKNOWN_UI_NODE =
"Unknown subclass of UiNode: ";
private final ObjectMapper mapper = new ObjectMapper();
private ServiceDirectory directory;
......@@ -87,7 +99,10 @@ class Topo2Jsonifier {
portStatsService = directory.get(PortStatisticsService.class);
topologyService = directory.get(TopologyService.class);
tunnelService = directory.get(TunnelService.class);
}
// for unit testing
Topo2Jsonifier() {
}
private ObjectNode objectNode() {
......@@ -155,47 +170,104 @@ class Topo2Jsonifier {
* Returns a JSON representation of the region to display in the topology
* view.
*
* @param region the region to transform to JSON
* @param region the region to transform to JSON
* @param subRegions the subregions within this region
* @return a JSON representation of the data
*/
ObjectNode region(UiRegion region) {
ObjectNode region(UiRegion region, Set<UiRegion> subRegions) {
ObjectNode payload = objectNode();
if (region == null) {
payload.put("note", "no-region");
return payload;
}
payload.put("id", region.idAsString());
payload.set("subregions", jsonSubRegions(subRegions));
payload.put("id", region.id().toString());
List<String> layerTags = region.layerOrder();
List<Set<UiNode>> splitDevices = splitByLayer(layerTags, region.devices());
List<Set<UiNode>> splitHosts = splitByLayer(layerTags, region.hosts());
Set<UiLink> links = region.links();
ArrayNode layerOrder = arrayNode();
payload.set("layerOrder", layerOrder);
region.layerOrder().forEach(layerOrder::add);
payload.set("devices", jsonGrouped(splitDevices));
payload.set("hosts", jsonGrouped(splitHosts));
payload.set("links", jsonLinks(links));
payload.set("layerOrder", jsonStrings(layerTags));
ArrayNode devices = arrayNode();
payload.set("devices", devices);
for (UiDevice device : region.devices()) {
devices.add(json(device));
}
return payload;
}
ArrayNode hosts = arrayNode();
payload.set("hosts", hosts);
for (UiHost host : region.hosts()) {
hosts.add(json(host));
private ArrayNode jsonSubRegions(Set<UiRegion> subregions) {
ArrayNode kids = arrayNode();
if (subregions != null) {
subregions.forEach(s -> kids.add(jsonClosedRegion(s)));
}
return kids;
}
ArrayNode links = arrayNode();
payload.set("links", links);
for (UiLink link : region.links()) {
links.add(json(link));
}
private ArrayNode jsonStrings(List<String> strings) {
ArrayNode array = arrayNode();
strings.forEach(array::add);
return array;
}
private ArrayNode jsonLinks(Set<UiLink> links) {
ArrayNode result = arrayNode();
links.forEach(lnk -> result.add(json(lnk)));
return result;
}
private ArrayNode jsonGrouped(List<Set<UiNode>> groupedNodes) {
ArrayNode result = arrayNode();
groupedNodes.forEach(g -> {
ArrayNode subset = arrayNode();
g.forEach(n -> subset.add(json(n)));
result.add(subset);
});
return result;
}
/**
* Returns a JSON payload that encapsulates the devices, hosts, links that
* do not belong to any region.
*
* @param oDevices orphan devices
* @param oHosts orphan hosts
* @param oLinks orphan links
* @param layerTags layer tags
* @return a JSON representation of the data
*/
ObjectNode orphans(Set<UiDevice> oDevices, Set<UiHost> oHosts,
Set<UiLink> oLinks, List<String> layerTags) {
ObjectNode payload = objectNode();
List<Set<UiNode>> splitDevices = splitByLayer(layerTags, oDevices);
List<Set<UiNode>> splitHosts = splitByLayer(layerTags, oHosts);
payload.set("devices", jsonGrouped(splitDevices));
payload.set("hosts", jsonGrouped(splitHosts));
payload.set("links", jsonLinks(oLinks));
payload.set("layerOrder", jsonStrings(layerTags));
return payload;
}
private ObjectNode json(UiNode node) {
if (node instanceof UiRegion) {
return jsonClosedRegion((UiRegion) node);
}
if (node instanceof UiDevice) {
return json((UiDevice) node);
}
if (node instanceof UiHost) {
return json((UiHost) node);
}
throw new IllegalStateException(E_UNKNOWN_UI_NODE + node.getClass());
}
private ObjectNode json(UiDevice device) {
ObjectNode node = objectNode()
.put("id", device.id().toString())
.put("id", device.idAsString())
.put("type", device.type())
.put("online", device.isOnline())
.put("master", device.master().toString())
......@@ -216,7 +288,7 @@ class Topo2Jsonifier {
private ObjectNode json(UiHost host) {
return objectNode()
.put("id", host.id().toString())
.put("id", host.idAsString())
.put("layer", host.layer());
// TODO: complete host details
}
......@@ -224,9 +296,101 @@ class Topo2Jsonifier {
private ObjectNode json(UiLink link) {
return objectNode()
.put("id", link.id().toString());
.put("id", link.idAsString());
// TODO: complete link details
}
private ObjectNode jsonClosedRegion(UiRegion region) {
return objectNode()
.put("id", region.idAsString());
// TODO: complete closed-region details
}
/**
* Returns a JSON array representation of a list of regions. Note that the
* information about each region is limited to what needs to be used to
* show the regions as nodes on the view.
*
* @param regions the regions
* @return a JSON representation of the minimal region information
*/
public ArrayNode closedRegions(Set<UiRegion> regions) {
ArrayNode array = arrayNode();
for (UiRegion r : regions) {
array.add(jsonClosedRegion(r));
}
return array;
}
/**
* Returns a JSON array representation of a list of devices.
*
* @param devices the devices
* @return a JSON representation of the devices
*/
public ArrayNode devices(Set<UiDevice> devices) {
ArrayNode array = arrayNode();
for (UiDevice device : devices) {
array.add(json(device));
}
return array;
}
/**
* Returns a JSON array representation of a list of hosts.
*
* @param hosts the hosts
* @return a JSON representation of the hosts
*/
public ArrayNode hosts(Set<UiHost> hosts) {
ArrayNode array = arrayNode();
for (UiHost host : hosts) {
array.add(json(host));
}
return array;
}
/**
* Returns a JSON array representation of a list of links.
*
* @param links the links
* @return a JSON representation of the links
*/
public ArrayNode links(Set<UiLink> links) {
ArrayNode array = arrayNode();
for (UiLink link : links) {
array.add(json(link));
}
return array;
}
// package-private for unit testing
List<Set<UiNode>> splitByLayer(List<String> layerTags,
Set<? extends UiNode> nodes) {
final int nLayers = layerTags.size();
if (!layerTags.get(nLayers - 1).equals(LAYER_DEFAULT)) {
throw new IllegalArgumentException(E_DEF_NOT_LAST);
}
List<Set<UiNode>> splitList = new ArrayList<>(layerTags.size());
Map<String, Set<UiNode>> byLayer = new HashMap<>(layerTags.size());
for (String tag : layerTags) {
Set<UiNode> set = new HashSet<>();
byLayer.put(tag, set);
splitList.add(set);
}
for (UiNode n : nodes) {
String which = n.layer();
if (!layerTags.contains(which)) {
which = LAYER_DEFAULT;
}
byLayer.get(which).add(n);
}
return splitList;
}
}
......
......@@ -17,6 +17,7 @@
package org.onosproject.ui.impl.topo;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.onlab.osgi.ServiceDirectory;
import org.onosproject.ui.RequestHandler;
......@@ -24,6 +25,9 @@ import org.onosproject.ui.UiConnection;
import org.onosproject.ui.UiMessageHandler;
import org.onosproject.ui.impl.UiWebSocket;
import org.onosproject.ui.model.topo.UiClusterMember;
import org.onosproject.ui.model.topo.UiDevice;
import org.onosproject.ui.model.topo.UiHost;
import org.onosproject.ui.model.topo.UiLink;
import org.onosproject.ui.model.topo.UiRegion;
import org.onosproject.ui.model.topo.UiTopoLayout;
import org.slf4j.Logger;
......@@ -31,6 +35,9 @@ import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
/*
NOTES:
......@@ -58,11 +65,14 @@ public class Topo2ViewMessageHandler extends UiMessageHandler {
private static final String TOPO2_STOP = "topo2Stop";
// === Outbound event identifiers
private static final String ALL_INSTANCES = "topo2AllInstances";
private static final String CURRENT_LAYOUT = "topo2CurrentLayout";
private static final String CURRENT_REGION = "topo2CurrentRegion";
private static final String ALL_INSTANCES = "topo2AllInstances";
private static final String PEER_REGIONS = "topo2PeerRegions";
private static final String ORPHANS = "topo2Orphans";
private static final String TOPO_START_DONE = "topo2StartDone";
private UiTopoSession topoSession;
private Topo2Jsonifier t2json;
......@@ -99,18 +109,36 @@ public class Topo2ViewMessageHandler extends UiMessageHandler {
log.debug("topo2Start: {}", payload);
// this is the list of ONOS cluster members
List<UiClusterMember> instances = topoSession.getAllInstances();
sendMessage(ALL_INSTANCES, t2json.instances(instances));
// this is the layout that the user has chosen to display
UiTopoLayout currentLayout = topoSession.currentLayout();
sendMessage(CURRENT_LAYOUT, t2json.layout(currentLayout));
// this is the region that is associated with the current layout
// this message includes details of the sub-regions, devices,
// hosts, and links within the region
// (as well as layer-order hints)
UiRegion region = topoSession.getRegion(currentLayout);
sendMessage(CURRENT_REGION, t2json.region(region));
// TODO: send information about devices/hosts/links in non-region
// TODO: send information about "linked, peer" regions
Set<UiRegion> kids = topoSession.getSubRegions(currentLayout);
sendMessage(CURRENT_REGION, t2json.region(region, kids));
// these are the regions that are siblings to this one
Set<UiRegion> peers = topoSession.getPeerRegions(currentLayout);
ObjectNode peersPayload = objectNode();
peersPayload.set("peers", t2json.closedRegions(peers));
sendMessage(PEER_REGIONS, peersPayload);
// return devices, hosts, links belonging to no region
Set<UiDevice> oDevices = topoSession.getOrphanDevices();
Set<UiHost> oHosts = topoSession.getOrphanHosts();
Set<UiLink> oLinks = topoSession.getOrphanLinks();
List<String> oLayers = getOrphanLayerOrder();
sendMessage(ORPHANS, t2json.orphans(oDevices, oHosts, oLinks, oLayers));
// finally, tell the UI that we are done
sendMessage(TOPO_START_DONE, null);
......@@ -122,6 +150,16 @@ public class Topo2ViewMessageHandler extends UiMessageHandler {
// sendAllHosts();
// sendTopoStartDone();
}
}
// TODO: we need to decide on how this should really get populated.
// For example, to be "backward compatible", this should really be
// [ LAYER_OPTICAL, LAYER_PACKET, LAYER_DEFAULT ]
private List<String> getOrphanLayerOrder() {
// NOTE that LAYER_DEFAULT must always be last in the array
return ImmutableList.of(LAYER_DEFAULT);
}
private final class Topo2Stop extends RequestHandler {
......
......@@ -22,12 +22,17 @@ import org.onosproject.ui.impl.topo.model.UiModelEvent;
import org.onosproject.ui.impl.topo.model.UiModelListener;
import org.onosproject.ui.impl.topo.model.UiSharedTopologyModel;
import org.onosproject.ui.model.topo.UiClusterMember;
import org.onosproject.ui.model.topo.UiDevice;
import org.onosproject.ui.model.topo.UiHost;
import org.onosproject.ui.model.topo.UiLink;
import org.onosproject.ui.model.topo.UiRegion;
import org.onosproject.ui.model.topo.UiTopoLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* Coordinates with the {@link UiTopoLayoutService} to access
......@@ -44,6 +49,7 @@ import java.util.List;
* interact with it when topo-related events come in from the client.
*/
public class UiTopoSession implements UiModelListener {
private final Logger log = LoggerFactory.getLogger(getClass());
private final UiWebSocket webSocket;
......@@ -73,6 +79,13 @@ public class UiTopoSession implements UiModelListener {
this.layoutService = layoutService;
}
// constructs a neutered instance, for unit testing
UiTopoSession() {
webSocket = null;
username = null;
sharedModel = null;
}
/**
* Initializes the session; registering with the shared model.
*/
......@@ -154,6 +167,68 @@ public class UiTopoSession implements UiModelListener {
* @return region that the layout is based upon
*/
public UiRegion getRegion(UiTopoLayout layout) {
return sharedModel.getRegion(layout);
return sharedModel.getRegion(layout.regionId());
}
/**
* Returns the regions that are "peers" to this region. That is, based on
* the layout the user is viewing, all the regions that are associated with
* layouts that are children of the parent layout to this layout.
*
* @param layout the layout being viewed
* @return all regions that are "siblings" to this layout's region
*/
public Set<UiRegion> getPeerRegions(UiTopoLayout layout) {
UiRegion currentRegion = getRegion(layout);
// TODO: consult topo layout service to get hierarchy info...
// TODO: then consult shared model to get regions
return Collections.emptySet();
}
/**
* Returns the subregions of the region in the specified layout.
*
* @param layout the layout being viewed
* @return all regions that are "contained within" this layout's region
*/
public Set<UiRegion> getSubRegions(UiTopoLayout layout) {
UiRegion currentRegion = getRegion(layout);
// TODO: consult topo layout service to get child layouts...
// TODO: then consult shared model to get regions
return Collections.emptySet();
}
/**
* Returns all devices that are not in a region.
*
* @return all devices not in a region
*/
public Set<UiDevice> getOrphanDevices() {
// TODO: get devices with no region
return Collections.emptySet();
}
/**
* Returns all hosts that are not in a region.
*
* @return all hosts not in a region
*/
public Set<UiHost> getOrphanHosts() {
// TODO: get hosts with no region
return Collections.emptySet();
}
/**
* Returns all links that are not in a region.
*
* @return all links not in a region
*/
public Set<UiLink> getOrphanLinks() {
// TODO: get links with no region
return Collections.emptySet();
}
}
......
......@@ -55,6 +55,7 @@ import org.onosproject.net.link.LinkListener;
import org.onosproject.net.link.LinkService;
import org.onosproject.net.region.Region;
import org.onosproject.net.region.RegionEvent;
import org.onosproject.net.region.RegionId;
import org.onosproject.net.region.RegionListener;
import org.onosproject.net.region.RegionService;
import org.onosproject.net.statistic.StatisticService;
......@@ -62,15 +63,11 @@ import org.onosproject.net.topology.TopologyService;
import org.onosproject.ui.impl.topo.UiTopoSession;
import org.onosproject.ui.model.ServiceBundle;
import org.onosproject.ui.model.topo.UiClusterMember;
import org.onosproject.ui.model.topo.UiElement;
import org.onosproject.ui.model.topo.UiRegion;
import org.onosproject.ui.model.topo.UiTopoLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
......@@ -210,23 +207,14 @@ public final class UiSharedTopologyModel
return cache.getAllClusterMembers();
}
public Set<UiElement> getElements(UiTopoLayout layout) {
Set<UiElement> results = new HashSet<>();
// TODO: figure out how to extract the appropriate nodes
// from the cache, for the given layout.
return results;
}
/**
* Returns the region for the given layout.
* Returns the region for the given identifier.
*
* @param layout layout filter
* @return the region the layout is based upon
* @param id region identifier
* @return the region
*/
public UiRegion getRegion(UiTopoLayout layout) {
return cache.accessRegion(layout.regionId());
public UiRegion getRegion(RegionId id) {
return cache.accessRegion(id);
}
// =====================================================================
......
/*
* 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;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.junit.Test;
import org.onosproject.ui.impl.AbstractUiImplTest;
import org.onosproject.ui.model.topo.UiNode;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
import static org.onosproject.ui.model.topo.UiNode.LAYER_OPTICAL;
import static org.onosproject.ui.model.topo.UiNode.LAYER_PACKET;
/**
* Unit tests for {@link Topo2ViewMessageHandler}.
*/
public class Topo2JsonifierTest extends AbstractUiImplTest {
// mock node class for testing
private static class MockNode extends UiNode {
private final String id;
MockNode(String id, String layer) {
this.id = id;
setLayer(layer);
}
@Override
public String idAsString() {
return id;
}
@Override
public String toString() {
return id;
}
}
private static final List<String> ALL_TAGS = ImmutableList.of(
LAYER_OPTICAL, LAYER_PACKET, LAYER_DEFAULT
);
private static final List<String> PKT_DEF_TAGS = ImmutableList.of(
LAYER_PACKET, LAYER_DEFAULT
);
private static final List<String> DEF_TAG_ONLY = ImmutableList.of(
LAYER_DEFAULT
);
private static final MockNode NODE_A = new MockNode("A-O", LAYER_OPTICAL);
private static final MockNode NODE_B = new MockNode("B-P", LAYER_PACKET);
private static final MockNode NODE_C = new MockNode("C-O", LAYER_OPTICAL);
private static final MockNode NODE_D = new MockNode("D-D", LAYER_DEFAULT);
private static final MockNode NODE_E = new MockNode("E-P", LAYER_PACKET);
private static final MockNode NODE_F = new MockNode("F-r", "random");
private static final Set<MockNode> NODES = ImmutableSet.of(
NODE_A, NODE_B, NODE_C, NODE_D, NODE_E, NODE_F
);
private Topo2Jsonifier t2 = new Topo2Jsonifier();
@Test
public void threeLayers() {
print("threeLayers()");
List<Set<UiNode>> result = t2.splitByLayer(ALL_TAGS, NODES);
print(result);
assertEquals("wrong split size", 3, result.size());
Set<UiNode> opt = result.get(0);
Set<UiNode> pkt = result.get(1);
Set<UiNode> def = result.get(2);
assertEquals("opt bad size", 2, opt.size());
assertEquals("missing node A", true, opt.contains(NODE_A));
assertEquals("missing node C", true, opt.contains(NODE_C));
assertEquals("pkt bad size", 2, pkt.size());
assertEquals("missing node B", true, pkt.contains(NODE_B));
assertEquals("missing node E", true, pkt.contains(NODE_E));
assertEquals("def bad size", 2, def.size());
assertEquals("missing node D", true, def.contains(NODE_D));
assertEquals("missing node F", true, def.contains(NODE_F));
}
@Test
public void twoLayers() {
print("twoLayers()");
List<Set<UiNode>> result = t2.splitByLayer(PKT_DEF_TAGS, NODES);
print(result);
assertEquals("wrong split size", 2, result.size());
Set<UiNode> pkt = result.get(0);
Set<UiNode> def = result.get(1);
assertEquals("pkt bad size", 2, pkt.size());
assertEquals("missing node B", true, pkt.contains(NODE_B));
assertEquals("missing node E", true, pkt.contains(NODE_E));
assertEquals("def bad size", 4, def.size());
assertEquals("missing node D", true, def.contains(NODE_D));
assertEquals("missing node F", true, def.contains(NODE_F));
assertEquals("missing node A", true, def.contains(NODE_A));
assertEquals("missing node C", true, def.contains(NODE_C));
}
@Test
public void oneLayer() {
print("oneLayer()");
List<Set<UiNode>> result = t2.splitByLayer(DEF_TAG_ONLY, NODES);
print(result);
assertEquals("wrong split size", 1, result.size());
Set<UiNode> def = result.get(0);
assertEquals("def bad size", 6, def.size());
assertEquals("missing node D", true, def.contains(NODE_D));
assertEquals("missing node F", true, def.contains(NODE_F));
assertEquals("missing node A", true, def.contains(NODE_A));
assertEquals("missing node C", true, def.contains(NODE_C));
assertEquals("missing node B", true, def.contains(NODE_B));
assertEquals("missing node E", true, def.contains(NODE_E));
}
}