blob: 9e8fcc74bb4106a77d873fbe472eec9cc230a36a [file] [log] [blame]
Thomas Vachuska329af532015-03-10 02:08:33 -07001/*
2 * Copyright 2015 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.onosproject.ui.impl;
17
18import com.fasterxml.jackson.databind.JsonNode;
19import com.fasterxml.jackson.databind.ObjectMapper;
20import com.fasterxml.jackson.databind.node.ArrayNode;
21import com.fasterxml.jackson.databind.node.ObjectNode;
22import org.onlab.osgi.ServiceDirectory;
23import org.onlab.packet.IpAddress;
24import org.onosproject.cluster.ClusterEvent;
25import org.onosproject.cluster.ClusterService;
26import org.onosproject.cluster.ControllerNode;
27import org.onosproject.cluster.NodeId;
28import org.onosproject.core.CoreService;
29import org.onosproject.mastership.MastershipService;
30import org.onosproject.net.Annotated;
31import org.onosproject.net.AnnotationKeys;
32import org.onosproject.net.Annotations;
33import org.onosproject.net.ConnectPoint;
34import org.onosproject.net.DefaultEdgeLink;
35import org.onosproject.net.Device;
36import org.onosproject.net.DeviceId;
37import org.onosproject.net.EdgeLink;
38import org.onosproject.net.Host;
39import org.onosproject.net.HostId;
40import org.onosproject.net.HostLocation;
41import org.onosproject.net.Link;
42import org.onosproject.net.LinkKey;
43import org.onosproject.net.PortNumber;
44import org.onosproject.net.device.DeviceEvent;
45import org.onosproject.net.device.DeviceService;
46import org.onosproject.net.flow.FlowEntry;
47import org.onosproject.net.flow.FlowRuleService;
48import org.onosproject.net.flow.TrafficTreatment;
49import org.onosproject.net.flow.instructions.Instruction;
50import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
51import org.onosproject.net.host.HostEvent;
52import org.onosproject.net.host.HostService;
53import org.onosproject.net.intent.Intent;
54import org.onosproject.net.intent.IntentService;
55import org.onosproject.net.intent.LinkCollectionIntent;
56import org.onosproject.net.intent.OpticalConnectivityIntent;
57import org.onosproject.net.intent.OpticalPathIntent;
58import org.onosproject.net.intent.PathIntent;
59import org.onosproject.net.link.LinkEvent;
60import org.onosproject.net.link.LinkService;
61import org.onosproject.net.provider.ProviderId;
62import org.onosproject.net.statistic.Load;
63import org.onosproject.net.statistic.StatisticService;
64import org.onosproject.net.topology.Topology;
65import org.onosproject.net.topology.TopologyService;
66import org.onosproject.ui.UiConnection;
67import org.onosproject.ui.UiMessageHandler;
68import org.slf4j.Logger;
69import org.slf4j.LoggerFactory;
70
71import java.text.DecimalFormat;
72import java.util.ArrayList;
73import java.util.Collection;
74import java.util.Collections;
75import java.util.HashMap;
76import java.util.HashSet;
77import java.util.Iterator;
78import java.util.List;
79import java.util.Map;
80import java.util.Set;
81import java.util.concurrent.ConcurrentHashMap;
82
83import static com.google.common.base.Preconditions.checkNotNull;
84import static com.google.common.base.Strings.isNullOrEmpty;
85import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ADDED;
86import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_REMOVED;
87import static org.onosproject.cluster.ControllerNode.State.ACTIVE;
88import static org.onosproject.net.DeviceId.deviceId;
89import static org.onosproject.net.HostId.hostId;
90import static org.onosproject.net.LinkKey.linkKey;
91import static org.onosproject.net.PortNumber.P0;
92import static org.onosproject.net.PortNumber.portNumber;
93import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
94import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_REMOVED;
95import static org.onosproject.net.host.HostEvent.Type.HOST_ADDED;
96import static org.onosproject.net.host.HostEvent.Type.HOST_REMOVED;
97import static org.onosproject.net.link.LinkEvent.Type.LINK_ADDED;
98import static org.onosproject.net.link.LinkEvent.Type.LINK_REMOVED;
99
100/**
101 * Facility for creating messages bound for the topology viewer.
102 */
103public abstract class TopologyViewMessageHandlerBase extends UiMessageHandler {
104
105 protected static final Logger log = LoggerFactory.getLogger(TopologyViewMessageHandlerBase.class);
106
107 private static final ProviderId PID = new ProviderId("core", "org.onosproject.core", true);
108 private static final String COMPACT = "%s/%s-%s/%s";
109
110 private static final double KB = 1024;
111 private static final double MB = 1024 * KB;
112 private static final double GB = 1024 * MB;
113
114 private static final String GB_UNIT = "GB";
115 private static final String MB_UNIT = "MB";
116 private static final String KB_UNIT = "KB";
117 private static final String B_UNIT = "B";
118
119 protected ServiceDirectory directory;
120 protected ClusterService clusterService;
121 protected DeviceService deviceService;
122 protected LinkService linkService;
123 protected HostService hostService;
124 protected MastershipService mastershipService;
125 protected IntentService intentService;
126 protected FlowRuleService flowService;
127 protected StatisticService statService;
128 protected TopologyService topologyService;
129
130 protected final ObjectMapper mapper = new ObjectMapper();
131 private String version;
132
133 // TODO: extract into an external & durable state; good enough for now and demo
134 private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
135
136 /**
137 * Creates a new message handler for the specified set of message types.
138 *
139 * @param messageTypes set of message types
140 */
141 protected TopologyViewMessageHandlerBase(Set<String> messageTypes) {
142 super(messageTypes);
143 }
144
145 /**
146 * Returns read-only view of the meta-ui information.
147 *
148 * @return map of id to meta-ui mementos
149 */
150 static Map<String, ObjectNode> getMetaUi() {
151 return Collections.unmodifiableMap(metaUi);
152 }
153
154 @Override
155 public void init(UiConnection connection, ServiceDirectory directory) {
156 super.init(connection, directory);
157 this.directory = checkNotNull(directory, "Directory cannot be null");
158 clusterService = directory.get(ClusterService.class);
159 deviceService = directory.get(DeviceService.class);
160 linkService = directory.get(LinkService.class);
161 hostService = directory.get(HostService.class);
162 mastershipService = directory.get(MastershipService.class);
163 intentService = directory.get(IntentService.class);
164 flowService = directory.get(FlowRuleService.class);
165 statService = directory.get(StatisticService.class);
166 topologyService = directory.get(TopologyService.class);
167
168 String ver = directory.get(CoreService.class).version().toString();
169 version = ver.replace(".SNAPSHOT", "*").replaceFirst("~.*$", "");
170 }
171
172 // Retrieves the payload from the specified event.
173 protected ObjectNode payload(ObjectNode event) {
174 return (ObjectNode) event.path("payload");
175 }
176
177 // Returns the specified node property as a number
178 protected long number(ObjectNode node, String name) {
179 return node.path(name).asLong();
180 }
181
182 // Returns the specified node property as a string.
183 protected String string(ObjectNode node, String name) {
184 return node.path(name).asText();
185 }
186
187 // Returns the specified node property as a string.
188 protected String string(ObjectNode node, String name, String defaultValue) {
189 return node.path(name).asText(defaultValue);
190 }
191
192 // Returns the specified set of IP addresses as a string.
193 private String ip(Set<IpAddress> ipAddresses) {
194 Iterator<IpAddress> it = ipAddresses.iterator();
195 return it.hasNext() ? it.next().toString() : "unknown";
196 }
197
198 // Produces JSON structure from annotations.
199 private JsonNode props(Annotations annotations) {
200 ObjectNode props = mapper.createObjectNode();
201 if (annotations != null) {
202 for (String key : annotations.keys()) {
203 props.put(key, annotations.value(key));
204 }
205 }
206 return props;
207 }
208
209 // Produces an informational log message event bound to the client.
210 protected ObjectNode info(long id, String message) {
211 return message("info", id, message);
212 }
213
214 // Produces a warning log message event bound to the client.
215 protected ObjectNode warning(long id, String message) {
216 return message("warning", id, message);
217 }
218
219 // Produces an error log message event bound to the client.
220 protected ObjectNode error(long id, String message) {
221 return message("error", id, message);
222 }
223
224 // Produces a log message event bound to the client.
225 private ObjectNode message(String severity, long id, String message) {
226 return envelope("message", id,
227 mapper.createObjectNode()
228 .put("severity", severity)
229 .put("message", message));
230 }
231
232 // Puts the payload into an envelope and returns it.
233 protected ObjectNode envelope(String type, long sid, ObjectNode payload) {
234 ObjectNode event = mapper.createObjectNode();
235 event.put("event", type);
236 if (sid > 0) {
237 event.put("sid", sid);
238 }
239 event.set("payload", payload);
240 return event;
241 }
242
243 // Produces a set of all hosts listed in the specified JSON array.
244 protected Set<Host> getHosts(ArrayNode array) {
245 Set<Host> hosts = new HashSet<>();
246 if (array != null) {
247 for (JsonNode node : array) {
248 try {
249 addHost(hosts, hostId(node.asText()));
250 } catch (IllegalArgumentException e) {
251 log.debug("Skipping ID {}", node.asText());
252 }
253 }
254 }
255 return hosts;
256 }
257
258 // Adds the specified host to the set of hosts.
259 private void addHost(Set<Host> hosts, HostId hostId) {
260 Host host = hostService.getHost(hostId);
261 if (host != null) {
262 hosts.add(host);
263 }
264 }
265
266
267 // Produces a set of all devices listed in the specified JSON array.
268 protected Set<Device> getDevices(ArrayNode array) {
269 Set<Device> devices = new HashSet<>();
270 if (array != null) {
271 for (JsonNode node : array) {
272 try {
273 addDevice(devices, deviceId(node.asText()));
274 } catch (IllegalArgumentException e) {
275 log.debug("Skipping ID {}", node.asText());
276 }
277 }
278 }
279 return devices;
280 }
281
282 private void addDevice(Set<Device> devices, DeviceId deviceId) {
283 Device device = deviceService.getDevice(deviceId);
284 if (device != null) {
285 devices.add(device);
286 }
287 }
288
289 protected void addHover(Set<Host> hosts, Set<Device> devices, String hover) {
290 try {
291 addHost(hosts, hostId(hover));
292 } catch (IllegalArgumentException e) {
293 try {
294 addDevice(devices, deviceId(hover));
295 } catch (IllegalArgumentException ne) {
296 log.debug("Skipping ID {}", hover);
297 }
298 }
299 }
300
301 // Produces a cluster instance message to the client.
302 protected ObjectNode instanceMessage(ClusterEvent event, String messageType) {
303 ControllerNode node = event.subject();
304 int switchCount = mastershipService.getDevicesOf(node.id()).size();
305 ObjectNode payload = mapper.createObjectNode()
306 .put("id", node.id().toString())
307 .put("ip", node.ip().toString())
308 .put("online", clusterService.getState(node.id()) == ACTIVE)
309 .put("uiAttached", event.subject().equals(clusterService.getLocalNode()))
310 .put("switches", switchCount);
311
312 ArrayNode labels = mapper.createArrayNode();
313 labels.add(node.id().toString());
314 labels.add(node.ip().toString());
315
316 // Add labels, props and stuff the payload into envelope.
317 payload.set("labels", labels);
318 addMetaUi(node.id().toString(), payload);
319
320 String type = messageType != null ? messageType :
321 ((event.type() == INSTANCE_ADDED) ? "addInstance" :
322 ((event.type() == INSTANCE_REMOVED ? "removeInstance" :
323 "addInstance")));
324 return envelope(type, 0, payload);
325 }
326
327 // Produces a device event message to the client.
328 protected ObjectNode deviceMessage(DeviceEvent event) {
329 Device device = event.subject();
330 ObjectNode payload = mapper.createObjectNode()
331 .put("id", device.id().toString())
332 .put("type", device.type().toString().toLowerCase())
333 .put("online", deviceService.isAvailable(device.id()))
334 .put("master", master(device.id()));
335
336 // Generate labels: id, chassis id, no-label, optional-name
337 String name = device.annotations().value(AnnotationKeys.NAME);
338 ArrayNode labels = mapper.createArrayNode();
339 labels.add("");
340 labels.add(isNullOrEmpty(name) ? device.id().toString() : name);
341 labels.add(device.id().toString());
342
343 // Add labels, props and stuff the payload into envelope.
344 payload.set("labels", labels);
345 payload.set("props", props(device.annotations()));
346 addGeoLocation(device, payload);
347 addMetaUi(device.id().toString(), payload);
348
349 String type = (event.type() == DEVICE_ADDED) ? "addDevice" :
350 ((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice");
351 return envelope(type, 0, payload);
352 }
353
354 // Produces a link event message to the client.
355 protected ObjectNode linkMessage(LinkEvent event) {
356 Link link = event.subject();
357 ObjectNode payload = mapper.createObjectNode()
358 .put("id", compactLinkString(link))
359 .put("type", link.type().toString().toLowerCase())
360 .put("online", link.state() == Link.State.ACTIVE)
361 .put("linkWidth", 1.2)
362 .put("src", link.src().deviceId().toString())
363 .put("srcPort", link.src().port().toString())
364 .put("dst", link.dst().deviceId().toString())
365 .put("dstPort", link.dst().port().toString());
366 String type = (event.type() == LINK_ADDED) ? "addLink" :
367 ((event.type() == LINK_REMOVED) ? "removeLink" : "updateLink");
368 return envelope(type, 0, payload);
369 }
370
371 // Produces a host event message to the client.
372 protected ObjectNode hostMessage(HostEvent event) {
373 Host host = event.subject();
374 String hostType = host.annotations().value(AnnotationKeys.TYPE);
375 ObjectNode payload = mapper.createObjectNode()
376 .put("id", host.id().toString())
377 .put("type", isNullOrEmpty(hostType) ? "endstation" : hostType)
378 .put("ingress", compactLinkString(edgeLink(host, true)))
379 .put("egress", compactLinkString(edgeLink(host, false)));
380 payload.set("cp", hostConnect(mapper, host.location()));
381 payload.set("labels", labels(mapper, ip(host.ipAddresses()),
382 host.mac().toString()));
383 payload.set("props", props(host.annotations()));
384 addGeoLocation(host, payload);
385 addMetaUi(host.id().toString(), payload);
386
387 String type = (event.type() == HOST_ADDED) ? "addHost" :
388 ((event.type() == HOST_REMOVED) ? "removeHost" : "updateHost");
389 return envelope(type, 0, payload);
390 }
391
392 // Encodes the specified host location into a JSON object.
393 private ObjectNode hostConnect(ObjectMapper mapper, HostLocation location) {
394 return mapper.createObjectNode()
395 .put("device", location.deviceId().toString())
396 .put("port", location.port().toLong());
397 }
398
399 // Encodes the specified list of labels a JSON array.
400 private ArrayNode labels(ObjectMapper mapper, String... labels) {
401 ArrayNode json = mapper.createArrayNode();
402 for (String label : labels) {
403 json.add(label);
404 }
405 return json;
406 }
407
408 // Returns the name of the master node for the specified device id.
409 private String master(DeviceId deviceId) {
410 NodeId master = mastershipService.getMasterFor(deviceId);
411 return master != null ? master.toString() : "";
412 }
413
414 // Generates an edge link from the specified host location.
415 private EdgeLink edgeLink(Host host, boolean ingress) {
416 return new DefaultEdgeLink(PID, new ConnectPoint(host.id(), portNumber(0)),
417 host.location(), ingress);
418 }
419
420 // Adds meta UI information for the specified object.
421 private void addMetaUi(String id, ObjectNode payload) {
422 ObjectNode meta = metaUi.get(id);
423 if (meta != null) {
424 payload.set("metaUi", meta);
425 }
426 }
427
428 // Adds a geo location JSON to the specified payload object.
429 private void addGeoLocation(Annotated annotated, ObjectNode payload) {
430 Annotations annotations = annotated.annotations();
431 if (annotations == null) {
432 return;
433 }
434
435 String slat = annotations.value(AnnotationKeys.LATITUDE);
436 String slng = annotations.value(AnnotationKeys.LONGITUDE);
437 try {
438 if (slat != null && slng != null && !slat.isEmpty() && !slng.isEmpty()) {
439 double lat = Double.parseDouble(slat);
440 double lng = Double.parseDouble(slng);
441 ObjectNode loc = mapper.createObjectNode()
442 .put("type", "latlng").put("lat", lat).put("lng", lng);
443 payload.set("location", loc);
444 }
445 } catch (NumberFormatException e) {
446 log.warn("Invalid geo data latitude={}; longiture={}", slat, slng);
447 }
448 }
449
450 // Updates meta UI information for the specified object.
451 protected void updateMetaUi(ObjectNode event) {
452 ObjectNode payload = payload(event);
453 metaUi.put(string(payload, "id"), (ObjectNode) payload.path("memento"));
454 }
455
456 // Returns summary response.
457 protected ObjectNode summmaryMessage(long sid) {
458 Topology topology = topologyService.currentTopology();
459 return envelope("showSummary", sid,
460 json("ONOS Summary", "node",
461 new Prop("Devices", format(topology.deviceCount())),
462 new Prop("Links", format(topology.linkCount())),
463 new Prop("Hosts", format(hostService.getHostCount())),
464 new Prop("Topology SCCs", format(topology.clusterCount())),
465 new Separator(),
466 new Prop("Intents", format(intentService.getIntentCount())),
467 new Prop("Flows", format(flowService.getFlowRuleCount())),
468 new Prop("Version", version)));
469 }
470
471 // Returns device details response.
472 protected ObjectNode deviceDetails(DeviceId deviceId, long sid) {
473 Device device = deviceService.getDevice(deviceId);
474 Annotations annot = device.annotations();
475 String name = annot.value(AnnotationKeys.NAME);
476 int portCount = deviceService.getPorts(deviceId).size();
477 int flowCount = getFlowCount(deviceId);
478 return envelope("showDetails", sid,
479 json(isNullOrEmpty(name) ? deviceId.toString() : name,
480 device.type().toString().toLowerCase(),
481 new Prop("URI", deviceId.toString()),
482 new Prop("Vendor", device.manufacturer()),
483 new Prop("H/W Version", device.hwVersion()),
484 new Prop("S/W Version", device.swVersion()),
485 new Prop("Serial Number", device.serialNumber()),
486 new Prop("Protocol", annot.value(AnnotationKeys.PROTOCOL)),
487 new Separator(),
488 new Prop("Master", master(deviceId)),
489 new Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)),
490 new Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE)),
491 new Separator(),
492 new Prop("Ports", Integer.toString(portCount)),
493 new Prop("Flows", Integer.toString(flowCount))));
494 }
495
496 protected int getFlowCount(DeviceId deviceId) {
497 int count = 0;
498 Iterator<FlowEntry> it = flowService.getFlowEntries(deviceId).iterator();
499 while (it.hasNext()) {
500 count++;
501 it.next();
502 }
503 return count;
504 }
505
506 // Counts all entries that egress on the given device links.
507 protected Map<Link, Integer> getFlowCounts(DeviceId deviceId) {
508 List<FlowEntry> entries = new ArrayList<>();
509 Set<Link> links = new HashSet<>(linkService.getDeviceEgressLinks(deviceId));
510 Set<Host> hosts = hostService.getConnectedHosts(deviceId);
511 Iterator<FlowEntry> it = flowService.getFlowEntries(deviceId).iterator();
512 while (it.hasNext()) {
513 entries.add(it.next());
514 }
515
516 // Add all edge links to the set
517 if (hosts != null) {
518 for (Host host : hosts) {
519 links.add(new DefaultEdgeLink(host.providerId(),
520 new ConnectPoint(host.id(), P0),
521 host.location(), false));
522 }
523 }
524
525 Map<Link, Integer> counts = new HashMap<>();
526 for (Link link : links) {
527 counts.put(link, getEgressFlows(link, entries));
528 }
529 return counts;
530 }
531
532 // Counts all entries that egress on the link source port.
533 private Integer getEgressFlows(Link link, List<FlowEntry> entries) {
534 int count = 0;
535 PortNumber out = link.src().port();
536 for (FlowEntry entry : entries) {
537 TrafficTreatment treatment = entry.treatment();
538 for (Instruction instruction : treatment.instructions()) {
539 if (instruction.type() == Instruction.Type.OUTPUT &&
540 ((OutputInstruction) instruction).port().equals(out)) {
541 count++;
542 }
543 }
544 }
545 return count;
546 }
547
548
549 // Returns host details response.
550 protected ObjectNode hostDetails(HostId hostId, long sid) {
551 Host host = hostService.getHost(hostId);
552 Annotations annot = host.annotations();
553 String type = annot.value(AnnotationKeys.TYPE);
554 String name = annot.value(AnnotationKeys.NAME);
555 String vlan = host.vlan().toString();
556 return envelope("showDetails", sid,
557 json(isNullOrEmpty(name) ? hostId.toString() : name,
558 isNullOrEmpty(type) ? "endstation" : type,
559 new Prop("MAC", host.mac().toString()),
560 new Prop("IP", host.ipAddresses().toString().replaceAll("[\\[\\]]", "")),
561 new Prop("VLAN", vlan.equals("-1") ? "none" : vlan),
562 new Separator(),
563 new Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)),
564 new Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE))));
565 }
566
567
568 // Produces JSON message to trigger traffic overview visualization
569 protected ObjectNode trafficSummaryMessage(long sid) {
570 ObjectNode payload = mapper.createObjectNode();
571 ArrayNode paths = mapper.createArrayNode();
572 payload.set("paths", paths);
573
574 ObjectNode pathNodeN = mapper.createObjectNode();
575 ArrayNode linksNodeN = mapper.createArrayNode();
576 ArrayNode labelsN = mapper.createArrayNode();
577
578 pathNodeN.put("class", "plain").put("traffic", false);
579 pathNodeN.set("links", linksNodeN);
580 pathNodeN.set("labels", labelsN);
581 paths.add(pathNodeN);
582
583 ObjectNode pathNodeT = mapper.createObjectNode();
584 ArrayNode linksNodeT = mapper.createArrayNode();
585 ArrayNode labelsT = mapper.createArrayNode();
586
587 pathNodeT.put("class", "secondary").put("traffic", true);
588 pathNodeT.set("links", linksNodeT);
589 pathNodeT.set("labels", labelsT);
590 paths.add(pathNodeT);
591
592 for (BiLink link : consolidateLinks(linkService.getLinks())) {
593 boolean bi = link.two != null;
594 if (isInfrastructureEgress(link.one) ||
595 (bi && isInfrastructureEgress(link.two))) {
596 link.addLoad(statService.load(link.one));
597 link.addLoad(bi ? statService.load(link.two) : null);
598 if (link.hasTraffic) {
599 linksNodeT.add(compactLinkString(link.one));
600 labelsT.add(formatBytes(link.bytes));
601 } else {
602 linksNodeN.add(compactLinkString(link.one));
603 labelsN.add("");
604 }
605 }
606 }
607 return envelope("showTraffic", sid, payload);
608 }
609
610 private Collection<BiLink> consolidateLinks(Iterable<Link> links) {
611 Map<LinkKey, BiLink> biLinks = new HashMap<>();
612 for (Link link : links) {
613 addLink(biLinks, link);
614 }
615 return biLinks.values();
616 }
617
618 // Produces JSON message to trigger flow overview visualization
619 protected ObjectNode flowSummaryMessage(long sid, Set<Device> devices) {
620 ObjectNode payload = mapper.createObjectNode();
621 ArrayNode paths = mapper.createArrayNode();
622 payload.set("paths", paths);
623
624 for (Device device : devices) {
625 Map<Link, Integer> counts = getFlowCounts(device.id());
626 for (Link link : counts.keySet()) {
627 addLinkFlows(link, paths, counts.get(link));
628 }
629 }
630 return envelope("showTraffic", sid, payload);
631 }
632
633 private void addLinkFlows(Link link, ArrayNode paths, Integer count) {
634 ObjectNode pathNode = mapper.createObjectNode();
635 ArrayNode linksNode = mapper.createArrayNode();
636 ArrayNode labels = mapper.createArrayNode();
637 boolean noFlows = count == null || count == 0;
638 pathNode.put("class", noFlows ? "secondary" : "primary");
639 pathNode.put("traffic", false);
640 pathNode.set("links", linksNode.add(compactLinkString(link)));
641 pathNode.set("labels", labels.add(noFlows ? "" : (count.toString() +
642 (count == 1 ? " flow" : " flows"))));
643 paths.add(pathNode);
644 }
645
646
647 // Produces JSON message to trigger traffic visualization
648 protected ObjectNode trafficMessage(long sid, TrafficClass... trafficClasses) {
649 ObjectNode payload = mapper.createObjectNode();
650 ArrayNode paths = mapper.createArrayNode();
651 payload.set("paths", paths);
652
653 // Classify links based on their traffic traffic first...
654 Map<LinkKey, BiLink> biLinks = classifyLinkTraffic(trafficClasses);
655
656 // Then separate the links into their respective classes and send them out.
657 Map<String, ObjectNode> pathNodes = new HashMap<>();
658 for (BiLink biLink : biLinks.values()) {
659 boolean hasTraffic = biLink.hasTraffic;
660 String tc = (biLink.classes + (hasTraffic ? " animated" : "")).trim();
661 ObjectNode pathNode = pathNodes.get(tc);
662 if (pathNode == null) {
663 pathNode = mapper.createObjectNode()
664 .put("class", tc).put("traffic", hasTraffic);
665 pathNode.set("links", mapper.createArrayNode());
666 pathNode.set("labels", mapper.createArrayNode());
667 pathNodes.put(tc, pathNode);
668 paths.add(pathNode);
669 }
670 ((ArrayNode) pathNode.path("links")).add(compactLinkString(biLink.one));
671 ((ArrayNode) pathNode.path("labels")).add(hasTraffic ? formatBytes(biLink.bytes) : "");
672 }
673
674 return envelope("showTraffic", sid, payload);
675 }
676
677 // Classifies the link traffic according to the specified classes.
678 private Map<LinkKey, BiLink> classifyLinkTraffic(TrafficClass... trafficClasses) {
679 Map<LinkKey, BiLink> biLinks = new HashMap<>();
680 for (TrafficClass trafficClass : trafficClasses) {
681 for (Intent intent : trafficClass.intents) {
682 boolean isOptical = intent instanceof OpticalConnectivityIntent;
683 List<Intent> installables = intentService.getInstallableIntents(intent.key());
684 if (installables != null) {
685 for (Intent installable : installables) {
686 String type = isOptical ? trafficClass.type + " optical" : trafficClass.type;
687 if (installable instanceof PathIntent) {
688 classifyLinks(type, biLinks, trafficClass.showTraffic,
689 ((PathIntent) installable).path().links());
690 } else if (installable instanceof LinkCollectionIntent) {
691 classifyLinks(type, biLinks, trafficClass.showTraffic,
692 ((LinkCollectionIntent) installable).links());
693 } else if (installable instanceof OpticalPathIntent) {
694 classifyLinks(type, biLinks, trafficClass.showTraffic,
695 ((OpticalPathIntent) installable).path().links());
696 }
697 }
698 }
699 }
700 }
701 return biLinks;
702 }
703
704
705 // Adds the link segments (path or tree) associated with the specified
706 // connectivity intent
707 private void classifyLinks(String type, Map<LinkKey, BiLink> biLinks,
708 boolean showTraffic, Iterable<Link> links) {
709 if (links != null) {
710 for (Link link : links) {
711 BiLink biLink = addLink(biLinks, link);
712 if (isInfrastructureEgress(link)) {
713 if (showTraffic) {
714 biLink.addLoad(statService.load(link));
715 }
716 biLink.addClass(type);
717 }
718 }
719 }
720 }
721
722
723 private BiLink addLink(Map<LinkKey, BiLink> biLinks, Link link) {
724 LinkKey key = canonicalLinkKey(link);
725 BiLink biLink = biLinks.get(key);
726 if (biLink != null) {
727 biLink.setOther(link);
728 } else {
729 biLink = new BiLink(key, link);
730 biLinks.put(key, biLink);
731 }
732 return biLink;
733 }
734
735
736 // Adds the link segments (path or tree) associated with the specified
737 // connectivity intent
738 protected void addPathTraffic(ArrayNode paths, String type, String trafficType,
739 Iterable<Link> links) {
740 ObjectNode pathNode = mapper.createObjectNode();
741 ArrayNode linksNode = mapper.createArrayNode();
742
743 if (links != null) {
744 ArrayNode labels = mapper.createArrayNode();
745 boolean hasTraffic = false;
746 for (Link link : links) {
747 if (isInfrastructureEgress(link)) {
748 linksNode.add(compactLinkString(link));
749 Load load = statService.load(link);
750 String label = "";
751 if (load.rate() > 0) {
752 hasTraffic = true;
753 label = formatBytes(load.latest());
754 }
755 labels.add(label);
756 }
757 }
758 pathNode.put("class", hasTraffic ? type + " " + trafficType : type);
759 pathNode.put("traffic", hasTraffic);
760 pathNode.set("links", linksNode);
761 pathNode.set("labels", labels);
762 paths.add(pathNode);
763 }
764 }
765
766 // Poor-mans formatting to get the labels with byte counts looking nice.
767 private String formatBytes(long bytes) {
768 String unit;
769 double value;
770 if (bytes > GB) {
771 value = bytes / GB;
772 unit = GB_UNIT;
773 } else if (bytes > MB) {
774 value = bytes / MB;
775 unit = MB_UNIT;
776 } else if (bytes > KB) {
777 value = bytes / KB;
778 unit = KB_UNIT;
779 } else {
780 value = bytes;
781 unit = B_UNIT;
782 }
783 DecimalFormat format = new DecimalFormat("#,###.##");
784 return format.format(value) + " " + unit;
785 }
786
787 // Formats the given number into a string.
788 private String format(Number number) {
789 DecimalFormat format = new DecimalFormat("#,###");
790 return format.format(number);
791 }
792
793 private boolean isInfrastructureEgress(Link link) {
794 return link.src().elementId() instanceof DeviceId;
795 }
796
797 // Produces compact string representation of a link.
798 private static String compactLinkString(Link link) {
799 return String.format(COMPACT, link.src().elementId(), link.src().port(),
800 link.dst().elementId(), link.dst().port());
801 }
802
803 // Produces JSON property details.
804 private ObjectNode json(String id, String type, Prop... props) {
805 ObjectMapper mapper = new ObjectMapper();
806 ObjectNode result = mapper.createObjectNode()
807 .put("id", id).put("type", type);
808 ObjectNode pnode = mapper.createObjectNode();
809 ArrayNode porder = mapper.createArrayNode();
810 for (Prop p : props) {
811 porder.add(p.key);
812 pnode.put(p.key, p.value);
813 }
814 result.set("propOrder", porder);
815 result.set("props", pnode);
816 return result;
817 }
818
819 // Produces canonical link key, i.e. one that will match link and its inverse.
820 private LinkKey canonicalLinkKey(Link link) {
821 String sn = link.src().elementId().toString();
822 String dn = link.dst().elementId().toString();
823 return sn.compareTo(dn) < 0 ?
824 linkKey(link.src(), link.dst()) : linkKey(link.dst(), link.src());
825 }
826
827 // Representation of link and its inverse and any traffic data.
828 private class BiLink {
829 public final LinkKey key;
830 public final Link one;
831 public Link two;
832 public boolean hasTraffic = false;
833 public long bytes = 0;
834 public String classes = "";
835
836 BiLink(LinkKey key, Link link) {
837 this.key = key;
838 this.one = link;
839 }
840
841 void setOther(Link link) {
842 this.two = link;
843 }
844
845 void addLoad(Load load) {
846 if (load != null) {
847 this.hasTraffic = hasTraffic || load.rate() > 0;
848 this.bytes += load.latest();
849 }
850 }
851
852 void addClass(String trafficClass) {
853 classes = classes + " " + trafficClass;
854 }
855 }
856
857 // Auxiliary key/value carrier.
858 private class Prop {
859 public final String key;
860 public final String value;
861
862 protected Prop(String key, String value) {
863 this.key = key;
864 this.value = value;
865 }
866 }
867
868 // Auxiliary properties separator
869 private class Separator extends Prop {
870 protected Separator() {
871 super("-", "");
872 }
873 }
874
875 // Auxiliary carrier of data for requesting traffic message.
876 protected class TrafficClass {
877 public final boolean showTraffic;
878 public final String type;
879 public final Iterable<Intent> intents;
880
881 TrafficClass(String type, Iterable<Intent> intents) {
882 this(type, intents, false);
883 }
884
885 TrafficClass(String type, Iterable<Intent> intents, boolean showTraffic) {
886 this.type = type;
887 this.intents = intents;
888 this.showTraffic = showTraffic;
889 }
890 }
891
892}