blob: 9e023344bd1690e26070f5169e9f362577f4a42d [file] [log] [blame]
Simon Huntd5b96732016-07-08 13:22:27 -07001/*
2 * Copyright 2016-present 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 */
16
17package org.onosproject.ui.impl.topo;
18
Simon Huntc13082f2016-08-03 21:20:23 -070019import com.fasterxml.jackson.databind.JsonNode;
Simon Huntd5b96732016-07-08 13:22:27 -070020import com.fasterxml.jackson.databind.ObjectMapper;
21import com.fasterxml.jackson.databind.node.ArrayNode;
22import com.fasterxml.jackson.databind.node.ObjectNode;
23import org.onlab.osgi.ServiceDirectory;
Steven Burrows583f4be2016-11-04 14:06:50 +010024import org.onlab.packet.IpAddress;
Simon Huntd5b96732016-07-08 13:22:27 -070025import org.onosproject.cluster.ClusterService;
26import org.onosproject.cluster.NodeId;
27import org.onosproject.incubator.net.PortStatisticsService;
28import org.onosproject.incubator.net.tunnel.TunnelService;
29import org.onosproject.mastership.MastershipService;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070030import org.onosproject.net.Annotated;
31import org.onosproject.net.Annotations;
32import org.onosproject.net.Device;
Steven Burrows583f4be2016-11-04 14:06:50 +010033import org.onosproject.net.Host;
Simon Huntd5b96732016-07-08 13:22:27 -070034import org.onosproject.net.device.DeviceService;
35import org.onosproject.net.flow.FlowRuleService;
36import org.onosproject.net.host.HostService;
37import org.onosproject.net.intent.IntentService;
38import org.onosproject.net.link.LinkService;
Simon Huntd5b96732016-07-08 13:22:27 -070039import org.onosproject.net.statistic.StatisticService;
40import org.onosproject.net.topology.TopologyService;
Steven Burrowse7cc3082016-09-27 11:24:58 -070041import org.onosproject.ui.JsonUtils;
Simon Huntd5b96732016-07-08 13:22:27 -070042import org.onosproject.ui.model.topo.UiClusterMember;
43import org.onosproject.ui.model.topo.UiDevice;
44import org.onosproject.ui.model.topo.UiHost;
45import org.onosproject.ui.model.topo.UiLink;
Simon Hunt977aa052016-07-20 17:08:29 -070046import org.onosproject.ui.model.topo.UiNode;
Simon Huntd5b96732016-07-08 13:22:27 -070047import org.onosproject.ui.model.topo.UiRegion;
Simon Huntc13082f2016-08-03 21:20:23 -070048import org.onosproject.ui.model.topo.UiSynthLink;
Simon Huntd5b96732016-07-08 13:22:27 -070049import org.onosproject.ui.model.topo.UiTopoLayout;
Simon Hunt98189192016-07-29 19:02:27 -070050import org.slf4j.Logger;
51import org.slf4j.LoggerFactory;
Simon Huntd5b96732016-07-08 13:22:27 -070052
Simon Hunt977aa052016-07-20 17:08:29 -070053import java.util.ArrayList;
54import java.util.HashMap;
55import java.util.HashSet;
Simon Huntd5b96732016-07-08 13:22:27 -070056import java.util.List;
Simon Hunt977aa052016-07-20 17:08:29 -070057import java.util.Map;
58import java.util.Set;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070059import java.util.concurrent.ConcurrentHashMap;
Simon Huntd5b96732016-07-08 13:22:27 -070060
61import static com.google.common.base.Preconditions.checkNotNull;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070062import static org.onosproject.net.AnnotationKeys.LATITUDE;
63import static org.onosproject.net.AnnotationKeys.LONGITUDE;
Simon Hunt977aa052016-07-20 17:08:29 -070064import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
Simon Huntd5b96732016-07-08 13:22:27 -070065
66/**
67 * Facility for creating JSON messages to send to the topology view in the
68 * Web client.
69 */
70class Topo2Jsonifier {
71
Simon Hunt977aa052016-07-20 17:08:29 -070072 private static final String E_DEF_NOT_LAST =
73 "UiNode.LAYER_DEFAULT not last in layer list";
74 private static final String E_UNKNOWN_UI_NODE =
75 "Unknown subclass of UiNode: ";
76
Simon Hunt98189192016-07-29 19:02:27 -070077 private static final String REGION = "region";
78 private static final String DEVICE = "device";
79 private static final String HOST = "host";
80
81 private final Logger log = LoggerFactory.getLogger(getClass());
82
Simon Huntd5b96732016-07-08 13:22:27 -070083 private final ObjectMapper mapper = new ObjectMapper();
84
85 private ServiceDirectory directory;
86 private ClusterService clusterService;
87 private DeviceService deviceService;
88 private LinkService linkService;
89 private HostService hostService;
90 private MastershipService mastershipService;
91 private IntentService intentService;
92 private FlowRuleService flowService;
93 private StatisticService flowStatsService;
94 private PortStatisticsService portStatsService;
95 private TopologyService topologyService;
96 private TunnelService tunnelService;
97
98
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070099 // NOTE: we'll stick this here for now, but maybe there is a better home?
100 // (this is not distributed across the cluster)
101 private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
102
103
Simon Huntd5b96732016-07-08 13:22:27 -0700104 /**
105 * Creates an instance with a reference to the services directory, so that
106 * additional information about network elements may be looked up on
107 * on the fly.
108 *
109 * @param directory service directory
110 */
111 Topo2Jsonifier(ServiceDirectory directory) {
112 this.directory = checkNotNull(directory, "Directory cannot be null");
113
114 clusterService = directory.get(ClusterService.class);
115 deviceService = directory.get(DeviceService.class);
116 linkService = directory.get(LinkService.class);
117 hostService = directory.get(HostService.class);
118 mastershipService = directory.get(MastershipService.class);
119 intentService = directory.get(IntentService.class);
120 flowService = directory.get(FlowRuleService.class);
121 flowStatsService = directory.get(StatisticService.class);
122 portStatsService = directory.get(PortStatisticsService.class);
123 topologyService = directory.get(TopologyService.class);
124 tunnelService = directory.get(TunnelService.class);
Simon Hunt977aa052016-07-20 17:08:29 -0700125 }
Simon Huntd5b96732016-07-08 13:22:27 -0700126
Simon Hunt977aa052016-07-20 17:08:29 -0700127 // for unit testing
128 Topo2Jsonifier() {
Simon Huntd5b96732016-07-08 13:22:27 -0700129 }
130
131 private ObjectNode objectNode() {
132 return mapper.createObjectNode();
133 }
134
135 private ArrayNode arrayNode() {
136 return mapper.createArrayNode();
137 }
138
139 private String nullIsEmpty(Object o) {
140 return o == null ? "" : o.toString();
141 }
142
143
144 /**
145 * Returns a JSON representation of the cluster members (ONOS instances).
146 *
147 * @param instances the instance model objects
148 * @return a JSON representation of the data
149 */
150 ObjectNode instances(List<UiClusterMember> instances) {
151 NodeId local = clusterService.getLocalNode().id();
152 ObjectNode payload = objectNode();
153
154 ArrayNode members = arrayNode();
155 payload.set("members", members);
156 for (UiClusterMember member : instances) {
157 members.add(json(member, member.id().equals(local)));
158 }
159
160 return payload;
161 }
162
163 private ObjectNode json(UiClusterMember member, boolean isUiAttached) {
164 return objectNode()
165 .put("id", member.id().toString())
166 .put("ip", member.ip().toString())
167 .put("online", member.isOnline())
168 .put("ready", member.isReady())
169 .put("uiAttached", isUiAttached)
170 .put("switches", member.deviceCount());
171 }
172
173 /**
174 * Returns a JSON representation of the layout to use for displaying in
Simon Huntf836a872016-08-10 17:37:36 -0700175 * the topology view. The identifiers and names of regions from the
176 * current to the root is included, so that the bread-crumb widget can
177 * be rendered.
Simon Huntd5b96732016-07-08 13:22:27 -0700178 *
179 * @param layout the layout to transform
Simon Huntf836a872016-08-10 17:37:36 -0700180 * @param crumbs list of layouts in bread-crumb order
Simon Huntd5b96732016-07-08 13:22:27 -0700181 * @return a JSON representation of the data
182 */
Simon Huntf836a872016-08-10 17:37:36 -0700183 ObjectNode layout(UiTopoLayout layout, List<UiTopoLayout> crumbs) {
184 ObjectNode result = objectNode()
Simon Huntd5b96732016-07-08 13:22:27 -0700185 .put("id", layout.id().toString())
186 .put("parent", nullIsEmpty(layout.parent()))
187 .put("region", nullIsEmpty(layout.regionId()))
Simon Huntf836a872016-08-10 17:37:36 -0700188 .put("regionName", UiRegion.safeName(layout.region()));
189 addCrumbs(result, crumbs);
190 return result;
Simon Huntd5b96732016-07-08 13:22:27 -0700191 }
192
Simon Huntf836a872016-08-10 17:37:36 -0700193 private void addCrumbs(ObjectNode result, List<UiTopoLayout> crumbs) {
194 ArrayNode trail = arrayNode();
195 crumbs.forEach(c -> {
196 ObjectNode n = objectNode()
197 .put("id", c.regionId().toString())
198 .put("name", UiRegion.safeName(c.region()));
199 trail.add(n);
200 });
201 result.set("crumbs", trail);
Simon Huntd5b96732016-07-08 13:22:27 -0700202 }
203
204 /**
205 * Returns a JSON representation of the region to display in the topology
206 * view.
207 *
Simon Hunt977aa052016-07-20 17:08:29 -0700208 * @param region the region to transform to JSON
209 * @param subRegions the subregions within this region
Simon Huntc13082f2016-08-03 21:20:23 -0700210 * @param links the links within this region
Simon Huntd5b96732016-07-08 13:22:27 -0700211 * @return a JSON representation of the data
212 */
Simon Huntc13082f2016-08-03 21:20:23 -0700213 ObjectNode region(UiRegion region, Set<UiRegion> subRegions,
214 List<UiSynthLink> links) {
Simon Huntd5b96732016-07-08 13:22:27 -0700215 ObjectNode payload = objectNode();
Simon Huntd5b96732016-07-08 13:22:27 -0700216 if (region == null) {
217 payload.put("note", "no-region");
218 return payload;
219 }
Simon Hunt977aa052016-07-20 17:08:29 -0700220 payload.put("id", region.idAsString());
Simon Huntcd508a62016-10-27 12:47:24 -0700221 payload.set("subregions", jsonSubRegions(subRegions));
222 payload.set("links", jsonLinks(links));
Simon Huntc13082f2016-08-03 21:20:23 -0700223
Simon Hunt977aa052016-07-20 17:08:29 -0700224 List<String> layerTags = region.layerOrder();
225 List<Set<UiNode>> splitDevices = splitByLayer(layerTags, region.devices());
226 List<Set<UiNode>> splitHosts = splitByLayer(layerTags, region.hosts());
Simon Huntd5b96732016-07-08 13:22:27 -0700227
Simon Hunt977aa052016-07-20 17:08:29 -0700228 payload.set("devices", jsonGrouped(splitDevices));
229 payload.set("hosts", jsonGrouped(splitHosts));
Simon Hunt977aa052016-07-20 17:08:29 -0700230 payload.set("layerOrder", jsonStrings(layerTags));
Simon Huntd5b96732016-07-08 13:22:27 -0700231
232 return payload;
233 }
234
Simon Hunt977aa052016-07-20 17:08:29 -0700235 private ArrayNode jsonSubRegions(Set<UiRegion> subregions) {
236 ArrayNode kids = arrayNode();
Simon Huntc13082f2016-08-03 21:20:23 -0700237 subregions.forEach(s -> kids.add(jsonClosedRegion(s)));
Simon Hunt977aa052016-07-20 17:08:29 -0700238 return kids;
239 }
240
Simon Huntc13082f2016-08-03 21:20:23 -0700241 private JsonNode jsonLinks(List<UiSynthLink> links) {
242 ArrayNode synthLinks = arrayNode();
243 links.forEach(l -> synthLinks.add(json(l)));
244 return synthLinks;
245 }
246
Simon Hunt977aa052016-07-20 17:08:29 -0700247 private ArrayNode jsonStrings(List<String> strings) {
248 ArrayNode array = arrayNode();
249 strings.forEach(array::add);
250 return array;
251 }
252
Simon Hunt977aa052016-07-20 17:08:29 -0700253 private ArrayNode jsonGrouped(List<Set<UiNode>> groupedNodes) {
254 ArrayNode result = arrayNode();
255 groupedNodes.forEach(g -> {
256 ArrayNode subset = arrayNode();
257 g.forEach(n -> subset.add(json(n)));
258 result.add(subset);
259 });
260 return result;
261 }
262
Simon Hunt977aa052016-07-20 17:08:29 -0700263
264 private ObjectNode json(UiNode node) {
265 if (node instanceof UiRegion) {
266 return jsonClosedRegion((UiRegion) node);
267 }
268 if (node instanceof UiDevice) {
269 return json((UiDevice) node);
270 }
271 if (node instanceof UiHost) {
272 return json((UiHost) node);
273 }
274 throw new IllegalStateException(E_UNKNOWN_UI_NODE + node.getClass());
275 }
276
Simon Huntd5b96732016-07-08 13:22:27 -0700277 private ObjectNode json(UiDevice device) {
278 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700279 .put("id", device.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700280 .put("nodeType", DEVICE)
Simon Huntd5b96732016-07-08 13:22:27 -0700281 .put("type", device.type())
Simon Hunt3d712522016-08-11 11:20:44 -0700282 .put("online", deviceService.isAvailable(device.id()))
Simon Huntb1ce2602016-07-23 14:04:31 -0700283 .put("master", nullIsEmpty(device.master()))
Simon Huntd5b96732016-07-08 13:22:27 -0700284 .put("layer", device.layer());
285
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700286 Device d = device.backingDevice();
287
288 addProps(node, d);
289 addGeoLocation(node, d);
290 addMetaUi(node, device.idAsString());
Simon Huntd5b96732016-07-08 13:22:27 -0700291
292 return node;
293 }
294
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700295 private void addProps(ObjectNode node, Device dev) {
296 Annotations annot = dev.annotations();
297 ObjectNode props = objectNode();
298 if (annot != null) {
299 annot.keys().forEach(k -> props.put(k, annot.value(k)));
300 }
301 node.set("props", props);
302 }
Simon Huntd5b96732016-07-08 13:22:27 -0700303
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700304 private void addMetaUi(ObjectNode node, String metaInstanceId) {
305 ObjectNode meta = metaUi.get(metaInstanceId);
306 if (meta != null) {
307 node.set("metaUi", meta);
308 }
309 }
310
311 private void addGeoLocation(ObjectNode node, Annotated a) {
312 List<String> lngLat = getAnnotValues(a, LONGITUDE, LATITUDE);
313 if (lngLat != null) {
314 try {
315 double lng = Double.parseDouble(lngLat.get(0));
316 double lat = Double.parseDouble(lngLat.get(1));
317 ObjectNode loc = objectNode()
318 .put("type", "lnglat")
319 .put("lng", lng)
320 .put("lat", lat);
321 node.set("location", loc);
322
323 } catch (NumberFormatException e) {
324 log.warn("Invalid geo data: longitude={}, latitude={}",
325 lngLat.get(0), lngLat.get(1));
326 }
327 } else {
328 log.debug("No geo lng/lat for {}", a);
329 }
330 }
331
Steven Burrows583f4be2016-11-04 14:06:50 +0100332 private void addIps(ObjectNode node, Host h) {
333 Set<IpAddress> ips = h.ipAddresses();
334
335 ArrayNode a = arrayNode();
336 for (IpAddress ip : ips) {
337 a.add(ip.toString());
338 }
339
340 node.set("ips", a);
341 }
342
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700343 // return list of string values from annotated instance, for given keys
344 // return null if any keys are not present
345 List<String> getAnnotValues(Annotated a, String... annotKeys) {
346 List<String> result = new ArrayList<>(annotKeys.length);
347 for (String k : annotKeys) {
348 String v = a.annotations().value(k);
349 if (v == null) {
350 return null;
351 }
352 result.add(v);
353 }
354 return result;
355 }
356
357 // derive JSON object from annotations
358 private ObjectNode props(Annotations annotations) {
359 ObjectNode p = objectNode();
360 if (annotations != null) {
361 annotations.keys().forEach(k -> p.put(k, annotations.value(k)));
362 }
363 return p;
Simon Huntd5b96732016-07-08 13:22:27 -0700364 }
365
366 private ObjectNode json(UiHost host) {
Steven Burrows583f4be2016-11-04 14:06:50 +0100367 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700368 .put("id", host.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700369 .put("nodeType", HOST)
Simon Huntd5b96732016-07-08 13:22:27 -0700370 .put("layer", host.layer());
371 // TODO: complete host details
Steven Burrows583f4be2016-11-04 14:06:50 +0100372 Host h = host.backingHost();
373
374 addIps(node, h);
375 addGeoLocation(node, h);
376 addMetaUi(node, host.idAsString());
377
378 return node;
Simon Huntd5b96732016-07-08 13:22:27 -0700379 }
380
Simon Huntc13082f2016-08-03 21:20:23 -0700381 private ObjectNode json(UiSynthLink sLink) {
382 UiLink uLink = sLink.link();
Simon Hunt3d712522016-08-11 11:20:44 -0700383 ObjectNode data = objectNode()
Simon Huntc13082f2016-08-03 21:20:23 -0700384 .put("id", uLink.idAsString())
385 .put("epA", uLink.endPointA())
386 .put("epB", uLink.endPointB())
387 .put("type", uLink.type());
Simon Hunt3d712522016-08-11 11:20:44 -0700388 String pA = uLink.endPortA();
389 String pB = uLink.endPortB();
390 if (pA != null) {
391 data.put("portA", pA);
392 }
393 if (pB != null) {
394 data.put("portB", pB);
395 }
396 return data;
Simon Huntd5b96732016-07-08 13:22:27 -0700397 }
398
399
Simon Hunt977aa052016-07-20 17:08:29 -0700400 private ObjectNode jsonClosedRegion(UiRegion region) {
Steven Burrows482d9502016-09-27 11:24:58 -0700401 ObjectNode node = objectNode()
Simon Huntb1ce2602016-07-23 14:04:31 -0700402 .put("id", region.idAsString())
Simon Huntf836a872016-08-10 17:37:36 -0700403 .put("name", region.name())
Simon Hunt98189192016-07-29 19:02:27 -0700404 .put("nodeType", REGION)
Steven Burrows19e6e4f2016-10-05 13:27:07 -0500405 .put("nDevs", region.deviceCount())
406 .put("nHosts", region.hostCount());
Simon Hunt977aa052016-07-20 17:08:29 -0700407 // TODO: complete closed-region details
Steven Burrows482d9502016-09-27 11:24:58 -0700408
409 addMetaUi(node, region.idAsString());
410 return node;
Simon Hunt977aa052016-07-20 17:08:29 -0700411 }
412
Simon Hunt98189192016-07-29 19:02:27 -0700413 /**
414 * Returns a JSON array representation of a set of regions/devices. Note
415 * that the information is sufficient for showing regions as nodes.
416 *
417 * @param nodes the nodes
418 * @return a JSON representation of the nodes
419 */
420 public ArrayNode closedNodes(Set<UiNode> nodes) {
421 ArrayNode array = arrayNode();
Simon Huntc13082f2016-08-03 21:20:23 -0700422 for (UiNode node : nodes) {
Simon Hunt98189192016-07-29 19:02:27 -0700423 if (node instanceof UiRegion) {
424 array.add(jsonClosedRegion((UiRegion) node));
425 } else if (node instanceof UiDevice) {
426 array.add(json((UiDevice) node));
427 } else {
428 log.warn("Unexpected node instance: {}", node.getClass());
429 }
430 }
431 return array;
432 }
Simon Hunt977aa052016-07-20 17:08:29 -0700433
434 /**
435 * Returns a JSON array representation of a list of regions. Note that the
436 * information about each region is limited to what needs to be used to
437 * show the regions as nodes on the view.
438 *
439 * @param regions the regions
440 * @return a JSON representation of the minimal region information
441 */
442 public ArrayNode closedRegions(Set<UiRegion> regions) {
443 ArrayNode array = arrayNode();
444 for (UiRegion r : regions) {
445 array.add(jsonClosedRegion(r));
446 }
447 return array;
448 }
449
450 /**
451 * Returns a JSON array representation of a list of devices.
452 *
453 * @param devices the devices
454 * @return a JSON representation of the devices
455 */
456 public ArrayNode devices(Set<UiDevice> devices) {
457 ArrayNode array = arrayNode();
458 for (UiDevice device : devices) {
459 array.add(json(device));
460 }
461 return array;
462 }
463
464 /**
465 * Returns a JSON array representation of a list of hosts.
466 *
467 * @param hosts the hosts
468 * @return a JSON representation of the hosts
469 */
470 public ArrayNode hosts(Set<UiHost> hosts) {
471 ArrayNode array = arrayNode();
472 for (UiHost host : hosts) {
473 array.add(json(host));
474 }
475 return array;
476 }
477
Simon Hunt977aa052016-07-20 17:08:29 -0700478 // package-private for unit testing
479 List<Set<UiNode>> splitByLayer(List<String> layerTags,
480 Set<? extends UiNode> nodes) {
481 final int nLayers = layerTags.size();
482 if (!layerTags.get(nLayers - 1).equals(LAYER_DEFAULT)) {
483 throw new IllegalArgumentException(E_DEF_NOT_LAST);
484 }
485
486 List<Set<UiNode>> splitList = new ArrayList<>(layerTags.size());
487 Map<String, Set<UiNode>> byLayer = new HashMap<>(layerTags.size());
488
489 for (String tag : layerTags) {
490 Set<UiNode> set = new HashSet<>();
491 byLayer.put(tag, set);
492 splitList.add(set);
493 }
494
495 for (UiNode n : nodes) {
496 String which = n.layer();
497 if (!layerTags.contains(which)) {
498 which = LAYER_DEFAULT;
499 }
500 byLayer.get(which).add(n);
501 }
502
503 return splitList;
504 }
Steven Burrowse7cc3082016-09-27 11:24:58 -0700505
506 /**
507 * Stores the memento for an element.
508 * This method assumes the payload has an id String, memento ObjectNode
509 *
510 * @param payload event payload
511 */
512 void updateMeta(ObjectNode payload) {
513
514 String id = JsonUtils.string(payload, "id");
515 metaUi.put(id, JsonUtils.node(payload, "memento"));
516
517 log.debug("Storing metadata for {}", id);
518 }
Simon Huntd5b96732016-07-08 13:22:27 -0700519}