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