ONOS-1479 -- GUI - augmenting topology view for extensibility: WIP.
- Major refactoring of TopologyViewMessageHandler and related classes.
Change-Id: I920f7f9f7317f3987a9a8da35ac086e9f8cab8d3
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorObject.java b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorObject.java
new file mode 100644
index 0000000..0826655
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitorObject.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright 2015 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;
+
+import com.google.common.collect.ImmutableList;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+import org.onosproject.net.intent.FlowRuleIntent;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.LinkCollectionIntent;
+import org.onosproject.net.intent.OpticalConnectivityIntent;
+import org.onosproject.net.intent.OpticalPathIntent;
+import org.onosproject.net.intent.PathIntent;
+import org.onosproject.net.statistic.Load;
+import org.onosproject.ui.impl.topo.BiLink;
+import org.onosproject.ui.impl.topo.IntentSelection;
+import org.onosproject.ui.impl.topo.LinkStatsType;
+import org.onosproject.ui.impl.topo.NodeSelection;
+import org.onosproject.ui.impl.topo.ServicesBundle;
+import org.onosproject.ui.impl.topo.TopoUtils;
+import org.onosproject.ui.impl.topo.TopologyViewIntentFilter;
+import org.onosproject.ui.impl.topo.TrafficClass;
+import org.onosproject.ui.topo.Highlights;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
+import static org.onosproject.ui.impl.TrafficMonitorObject.Mode.IDLE;
+import static org.onosproject.ui.impl.TrafficMonitorObject.Mode.SEL_INTENT;
+import static org.onosproject.ui.topo.LinkHighlight.Flavor.PRIMARY_HIGHLIGHT;
+import static org.onosproject.ui.topo.LinkHighlight.Flavor.SECONDARY_HIGHLIGHT;
+
+/**
+ * Encapsulates the behavior of monitoring specific traffic patterns.
+ */
+public class TrafficMonitorObject {
+
+ // 4 Kilo Bytes as threshold
+ private static final double BPS_THRESHOLD = 4 * TopoUtils.KILO;
+
+ private static final Logger log =
+ LoggerFactory.getLogger(TrafficMonitorObject.class);
+
+ /**
+ * Designates the different modes of operation.
+ */
+ public enum Mode {
+ IDLE,
+ ALL_FLOW_TRAFFIC,
+ ALL_PORT_TRAFFIC,
+ DEV_LINK_FLOWS,
+ RELATED_INTENTS,
+ SEL_INTENT
+ }
+
+ private final long trafficPeriod;
+ private final ServicesBundle servicesBundle;
+ private final TopologyViewMessageHandler messageHandler;
+ private final TopologyViewIntentFilter intentFilter;
+
+ private final Timer timer = new Timer("topo-traffic");
+
+ private TimerTask trafficTask = null;
+ private Mode mode = IDLE;
+ private NodeSelection selectedNodes = null;
+ private IntentSelection selectedIntents = null;
+
+
+ /**
+ * Constructs a traffic monitor.
+ *
+ * @param trafficPeriod traffic task period in ms
+ * @param servicesBundle bundle of services
+ * @param messageHandler our message handler
+ */
+ public TrafficMonitorObject(long trafficPeriod,
+ ServicesBundle servicesBundle,
+ TopologyViewMessageHandler messageHandler) {
+ this.trafficPeriod = trafficPeriod;
+ this.servicesBundle = servicesBundle;
+ this.messageHandler = messageHandler;
+
+ intentFilter = new TopologyViewIntentFilter(servicesBundle);
+ }
+
+ // =======================================================================
+ // === API === // TODO: add javadocs
+
+ public synchronized void monitor(Mode mode) {
+ log.debug("monitor: {}", mode);
+ this.mode = mode;
+
+ switch (mode) {
+ case ALL_FLOW_TRAFFIC:
+ clearSelection();
+ scheduleTask();
+ sendAllFlowTraffic();
+ break;
+
+ case ALL_PORT_TRAFFIC:
+ clearSelection();
+ scheduleTask();
+ sendAllPortTraffic();
+ break;
+
+ case SEL_INTENT:
+ scheduleTask();
+ sendSelectedIntentTraffic();
+ break;
+
+ default:
+ log.debug("Unexpected call to monitor({})", mode);
+ clearAll();
+ break;
+ }
+ }
+
+ public synchronized void monitor(Mode mode, NodeSelection nodeSelection) {
+ log.debug("monitor: {} -- {}", mode, nodeSelection);
+ this.mode = mode;
+ this.selectedNodes = nodeSelection;
+
+ switch (mode) {
+ case DEV_LINK_FLOWS:
+ // only care about devices (not hosts)
+ if (selectedNodes.devices().isEmpty()) {
+ sendClearAll();
+ } else {
+ scheduleTask();
+ sendDeviceLinkFlows();
+ }
+ break;
+
+ case RELATED_INTENTS:
+ if (selectedNodes.none()) {
+ sendClearAll();
+ } else {
+ selectedIntents = new IntentSelection(selectedNodes, intentFilter);
+ if (selectedIntents.none()) {
+ sendClearAll();
+ } else {
+ sendSelectedIntents();
+ }
+ }
+ break;
+
+ default:
+ log.debug("Unexpected call to monitor({}, {})", mode, nodeSelection);
+ clearAll();
+ break;
+ }
+ }
+
+ public synchronized void monitor(Intent intent) {
+ log.debug("monitor intent: {}", intent.id());
+ selectedNodes = null;
+ selectedIntents = new IntentSelection(intent);
+ mode = SEL_INTENT;
+ scheduleTask();
+ sendSelectedIntentTraffic();
+ }
+
+ public synchronized void selectNextIntent() {
+ if (selectedIntents != null) {
+ selectedIntents.next();
+ sendSelectedIntents();
+ }
+ }
+
+ public synchronized void selectPreviousIntent() {
+ if (selectedIntents != null) {
+ selectedIntents.prev();
+ sendSelectedIntents();
+ }
+ }
+
+ public synchronized void pokeIntent() {
+ if (mode == SEL_INTENT) {
+ sendSelectedIntentTraffic();
+ }
+ }
+
+ public synchronized void stop() {
+ log.debug("STOP");
+ if (mode != IDLE) {
+ sendClearAll();
+ }
+ }
+
+
+ // =======================================================================
+ // === Helper methods ===
+
+ private void sendClearAll() {
+ clearAll();
+ sendClearHighlights();
+ }
+
+ private void clearAll() {
+ this.mode = IDLE;
+ clearSelection();
+ cancelTask();
+ }
+
+ private void clearSelection() {
+ selectedNodes = null;
+ selectedIntents = null;
+ }
+
+ private synchronized void scheduleTask() {
+ if (trafficTask == null) {
+ log.debug("Starting up background traffic task...");
+ trafficTask = new TrafficMonitor();
+ timer.schedule(trafficTask, trafficPeriod, trafficPeriod);
+ } else {
+ // TEMPORARY until we are sure this is working correctly
+ log.debug("(traffic task already running)");
+ }
+ }
+
+ private synchronized void cancelTask() {
+ if (trafficTask != null) {
+ trafficTask.cancel();
+ trafficTask = null;
+ }
+ }
+
+ // ---
+
+ private void sendAllFlowTraffic() {
+ log.debug("sendAllFlowTraffic");
+ sendHighlights(trafficSummary(LinkStatsType.FLOW_STATS));
+ }
+
+ private void sendAllPortTraffic() {
+ log.debug("sendAllPortTraffic");
+ sendHighlights(trafficSummary(LinkStatsType.PORT_STATS));
+ }
+
+ private void sendDeviceLinkFlows() {
+ log.debug("sendDeviceLinkFlows: {}", selectedNodes);
+ sendHighlights(deviceLinkFlows());
+ }
+
+ private void sendSelectedIntents() {
+ log.debug("sendSelectedIntents: {}", selectedIntents);
+ sendHighlights(intentGroup());
+ }
+
+ private void sendSelectedIntentTraffic() {
+ log.debug("sendSelectedIntentTraffic: {}", selectedIntents);
+ sendHighlights(intentTraffic());
+ }
+
+ private void sendClearHighlights() {
+ log.debug("sendClearHighlights");
+ sendHighlights(new Highlights());
+ }
+
+ private void sendHighlights(Highlights highlights) {
+ messageHandler.sendHighlights(highlights);
+ }
+
+
+ // =======================================================================
+ // === Generate messages in JSON object node format
+
+ private Highlights trafficSummary(LinkStatsType type) {
+ Highlights highlights = new Highlights();
+
+ // compile a set of bilinks (combining pairs of unidirectional links)
+ Map<LinkKey, BiLink> linkMap = new HashMap<>();
+ compileLinks(linkMap);
+ addEdgeLinks(linkMap);
+
+ for (BiLink blink : linkMap.values()) {
+ if (type == LinkStatsType.FLOW_STATS) {
+ attachFlowLoad(blink);
+ } else if (type == LinkStatsType.PORT_STATS) {
+ attachPortLoad(blink);
+ }
+
+ // we only want to report on links deemed to have traffic
+ if (blink.hasTraffic()) {
+ highlights.add(blink.generateHighlight(type));
+ }
+ }
+ return highlights;
+ }
+
+ // create highlights for links, showing flows for selected devices.
+ private Highlights deviceLinkFlows() {
+ Highlights highlights = new Highlights();
+
+ if (selectedNodes != null && !selectedNodes.devices().isEmpty()) {
+ // capture flow counts on bilinks
+ Map<LinkKey, BiLink> linkMap = new HashMap<>();
+
+ for (Device device : selectedNodes.devices()) {
+ Map<Link, Integer> counts = getLinkFlowCounts(device.id());
+ for (Link link : counts.keySet()) {
+ BiLink blink = TopoUtils.addLink(linkMap, link);
+ blink.addFlows(counts.get(link));
+ }
+ }
+
+ // now report on our collated links
+ for (BiLink blink : linkMap.values()) {
+ highlights.add(blink.generateHighlight(LinkStatsType.FLOW_COUNT));
+ }
+
+ }
+ return highlights;
+ }
+
+ private Highlights intentGroup() {
+ Highlights highlights = new Highlights();
+
+ if (selectedIntents != null && !selectedIntents.none()) {
+ // If 'all' intents are selected, they will all have primary
+ // highlighting; otherwise, the specifically selected intent will
+ // have primary highlighting, and the remainder will have secondary
+ // highlighting.
+ Set<Intent> primary;
+ Set<Intent> secondary;
+ int count = selectedIntents.size();
+
+ Set<Intent> allBut = new HashSet<>(selectedIntents.intents());
+ Intent current;
+
+ if (selectedIntents.all()) {
+ primary = allBut;
+ secondary = Collections.emptySet();
+ log.debug("Highlight all intents ({})", count);
+ } else {
+ current = selectedIntents.current();
+ primary = new HashSet<>();
+ primary.add(current);
+ allBut.remove(current);
+ secondary = allBut;
+ log.debug("Highlight intent: {} ([{}] of {})",
+ current.id(), selectedIntents.index(), count);
+ }
+ TrafficClass tc1 = new TrafficClass(PRIMARY_HIGHLIGHT, primary);
+ TrafficClass tc2 = new TrafficClass(SECONDARY_HIGHLIGHT, secondary);
+ // classify primary links after secondary (last man wins)
+ highlightIntents(highlights, tc2, tc1);
+ }
+ return highlights;
+ }
+
+ private Highlights intentTraffic() {
+ Highlights highlights = new Highlights();
+
+ if (selectedIntents != null && selectedIntents.single()) {
+ Intent current = selectedIntents.current();
+ Set<Intent> primary = new HashSet<>();
+ primary.add(current);
+ log.debug("Highlight traffic for intent: {} ([{}] of {})",
+ current.id(), selectedIntents.index(), selectedIntents.size());
+ TrafficClass tc1 = new TrafficClass(PRIMARY_HIGHLIGHT, primary, true);
+ highlightIntents(highlights, tc1);
+ }
+ return highlights;
+ }
+
+
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+ private void compileLinks(Map<LinkKey, BiLink> linkMap) {
+ servicesBundle.linkService().getLinks()
+ .forEach(link -> TopoUtils.addLink(linkMap, link));
+ }
+
+ private void addEdgeLinks(Map<LinkKey, BiLink> biLinks) {
+ servicesBundle.hostService().getHosts().forEach(host -> {
+ TopoUtils.addLink(biLinks, createEdgeLink(host, true));
+ TopoUtils.addLink(biLinks, createEdgeLink(host, false));
+ });
+ }
+
+ private Load getLinkFlowLoad(Link link) {
+ if (link != null && link.src().elementId() instanceof DeviceId) {
+ return servicesBundle.flowStatsService().load(link);
+ }
+ return null;
+ }
+
+ private void attachFlowLoad(BiLink link) {
+ link.addLoad(getLinkFlowLoad(link.one()));
+ link.addLoad(getLinkFlowLoad(link.two()));
+ }
+
+ private void attachPortLoad(BiLink link) {
+ // For bi-directional traffic links, use
+ // the max link rate of either direction
+ // (we choose 'one' since we know that is never null)
+ Link one = link.one();
+ Load egressSrc = servicesBundle.portStatsService().load(one.src());
+ Load egressDst = servicesBundle.portStatsService().load(one.dst());
+// link.addLoad(maxLoad(egressSrc, egressDst), BPS_THRESHOLD);
+ link.addLoad(maxLoad(egressSrc, egressDst), 10); // FIXME - debug only
+ }
+
+ private Load maxLoad(Load a, Load b) {
+ if (a == null) {
+ return b;
+ }
+ if (b == null) {
+ return a;
+ }
+ return a.rate() > b.rate() ? a : b;
+ }
+
+ // ---
+
+ // Counts all flow entries that egress on the links of the given device.
+ private Map<Link, Integer> getLinkFlowCounts(DeviceId deviceId) {
+ // get the flows for the device
+ List<FlowEntry> entries = new ArrayList<>();
+ for (FlowEntry flowEntry : servicesBundle.flowService().getFlowEntries(deviceId)) {
+ entries.add(flowEntry);
+ }
+
+ // get egress links from device, and include edge links
+ Set<Link> links = new HashSet<>(servicesBundle.linkService().getDeviceEgressLinks(deviceId));
+ Set<Host> hosts = servicesBundle.hostService().getConnectedHosts(deviceId);
+ if (hosts != null) {
+ for (Host host : hosts) {
+ links.add(createEdgeLink(host, false));
+ }
+ }
+
+ // compile flow counts per link
+ Map<Link, Integer> counts = new HashMap<>();
+ for (Link link : links) {
+ counts.put(link, getEgressFlows(link, entries));
+ }
+ return counts;
+ }
+
+ // Counts all entries that egress on the link source port.
+ private int getEgressFlows(Link link, List<FlowEntry> entries) {
+ int count = 0;
+ PortNumber out = link.src().port();
+ for (FlowEntry entry : entries) {
+ TrafficTreatment treatment = entry.treatment();
+ for (Instruction instruction : treatment.allInstructions()) {
+ if (instruction.type() == Instruction.Type.OUTPUT &&
+ ((OutputInstruction) instruction).port().equals(out)) {
+ count++;
+ }
+ }
+ }
+ return count;
+ }
+
+ // ---
+ private void highlightIntents(Highlights highlights,
+ TrafficClass... trafficClasses) {
+ Map<LinkKey, BiLink> linkMap = new HashMap<>();
+
+
+ for (TrafficClass trafficClass : trafficClasses) {
+ classifyLinkTraffic(linkMap, trafficClass);
+ }
+
+ for (BiLink blink : linkMap.values()) {
+ highlights.add(blink.generateHighlight(LinkStatsType.TAGGED));
+ }
+ }
+
+ private void classifyLinkTraffic(Map<LinkKey, BiLink> linkMap,
+ TrafficClass trafficClass) {
+ for (Intent intent : trafficClass.intents()) {
+ boolean isOptical = intent instanceof OpticalConnectivityIntent;
+ List<Intent> installables = servicesBundle.intentService()
+ .getInstallableIntents(intent.key());
+ Iterable<Link> links = null;
+
+ if (installables != null) {
+ for (Intent installable : installables) {
+
+ if (installable instanceof PathIntent) {
+ links = ((PathIntent) installable).path().links();
+ } else if (installable instanceof FlowRuleIntent) {
+ links = linkResources(installable);
+ } else if (installable instanceof LinkCollectionIntent) {
+ links = ((LinkCollectionIntent) installable).links();
+ } else if (installable instanceof OpticalPathIntent) {
+ links = ((OpticalPathIntent) installable).path().links();
+ }
+
+ classifyLinks(trafficClass, isOptical, linkMap, links);
+ }
+ }
+ }
+ }
+
+ private void classifyLinks(TrafficClass trafficClass, boolean isOptical,
+ Map<LinkKey, BiLink> linkMap,
+ Iterable<Link> links) {
+ if (links != null) {
+ for (Link link : links) {
+ BiLink blink = TopoUtils.addLink(linkMap, link);
+ if (trafficClass.showTraffic()) {
+ blink.addLoad(getLinkFlowLoad(link));
+ blink.setAntMarch(true);
+ }
+ blink.setOptical(isOptical);
+ blink.tagFlavor(trafficClass.flavor());
+ }
+ }
+ }
+
+ // Extracts links from the specified flow rule intent resources
+ private Collection<Link> linkResources(Intent installable) {
+ ImmutableList.Builder<Link> builder = ImmutableList.builder();
+ installable.resources().stream().filter(r -> r instanceof Link)
+ .forEach(r -> builder.add((Link) r));
+ return builder.build();
+ }
+
+ // =======================================================================
+ // === Background Task
+
+ // Provides periodic update of traffic information to the client
+ private class TrafficMonitor extends TimerTask {
+ @Override
+ public void run() {
+ try {
+ switch (mode) {
+ case ALL_FLOW_TRAFFIC:
+ sendAllFlowTraffic();
+ break;
+ case ALL_PORT_TRAFFIC:
+ sendAllPortTraffic();
+ break;
+ case DEV_LINK_FLOWS:
+ sendDeviceLinkFlows();
+ break;
+ case SEL_INTENT:
+ sendSelectedIntentTraffic();
+ break;
+
+ default:
+ // RELATED_INTENTS and IDLE modes should never invoke
+ // the background task, but if they do, they have
+ // nothing to do
+ break;
+ }
+
+ } catch (Exception e) {
+ log.warn("Unable to process traffic task due to {}", e.getMessage());
+ log.warn("Boom!", e);
+ }
+ }
+ }
+
+}