blob: 11aef1df27aba3a12ef2e54fbd88235f0b6152df [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;
24import org.onosproject.cluster.ClusterService;
25import org.onosproject.cluster.NodeId;
26import org.onosproject.incubator.net.PortStatisticsService;
27import org.onosproject.incubator.net.tunnel.TunnelService;
28import org.onosproject.mastership.MastershipService;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070029import org.onosproject.net.Annotated;
30import org.onosproject.net.Annotations;
31import org.onosproject.net.Device;
Simon Huntd5b96732016-07-08 13:22:27 -070032import org.onosproject.net.device.DeviceService;
33import org.onosproject.net.flow.FlowRuleService;
34import org.onosproject.net.host.HostService;
35import org.onosproject.net.intent.IntentService;
36import org.onosproject.net.link.LinkService;
Simon Huntd5b96732016-07-08 13:22:27 -070037import org.onosproject.net.statistic.StatisticService;
38import org.onosproject.net.topology.TopologyService;
Steven Burrowse7cc3082016-09-27 11:24:58 -070039import org.onosproject.ui.JsonUtils;
Simon Huntd5b96732016-07-08 13:22:27 -070040import org.onosproject.ui.model.topo.UiClusterMember;
41import org.onosproject.ui.model.topo.UiDevice;
42import org.onosproject.ui.model.topo.UiHost;
43import org.onosproject.ui.model.topo.UiLink;
Simon Hunt977aa052016-07-20 17:08:29 -070044import org.onosproject.ui.model.topo.UiNode;
Simon Huntd5b96732016-07-08 13:22:27 -070045import org.onosproject.ui.model.topo.UiRegion;
Simon Huntc13082f2016-08-03 21:20:23 -070046import org.onosproject.ui.model.topo.UiSynthLink;
Simon Huntd5b96732016-07-08 13:22:27 -070047import org.onosproject.ui.model.topo.UiTopoLayout;
Simon Hunt98189192016-07-29 19:02:27 -070048import org.slf4j.Logger;
49import org.slf4j.LoggerFactory;
Simon Huntd5b96732016-07-08 13:22:27 -070050
Simon Hunt977aa052016-07-20 17:08:29 -070051import java.util.ArrayList;
52import java.util.HashMap;
53import java.util.HashSet;
Simon Huntd5b96732016-07-08 13:22:27 -070054import java.util.List;
Simon Hunt977aa052016-07-20 17:08:29 -070055import java.util.Map;
56import java.util.Set;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070057import java.util.concurrent.ConcurrentHashMap;
Simon Huntd5b96732016-07-08 13:22:27 -070058
59import static com.google.common.base.Preconditions.checkNotNull;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070060import static org.onosproject.net.AnnotationKeys.LATITUDE;
61import static org.onosproject.net.AnnotationKeys.LONGITUDE;
Simon Hunt977aa052016-07-20 17:08:29 -070062import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
Simon Huntd5b96732016-07-08 13:22:27 -070063
64/**
65 * Facility for creating JSON messages to send to the topology view in the
66 * Web client.
67 */
68class Topo2Jsonifier {
69
Simon Hunt977aa052016-07-20 17:08:29 -070070 private static final String E_DEF_NOT_LAST =
71 "UiNode.LAYER_DEFAULT not last in layer list";
72 private static final String E_UNKNOWN_UI_NODE =
73 "Unknown subclass of UiNode: ";
74
Simon Hunt98189192016-07-29 19:02:27 -070075 private static final String REGION = "region";
76 private static final String DEVICE = "device";
77 private static final String HOST = "host";
78
79 private final Logger log = LoggerFactory.getLogger(getClass());
80
Simon Huntd5b96732016-07-08 13:22:27 -070081 private final ObjectMapper mapper = new ObjectMapper();
82
83 private ServiceDirectory directory;
84 private ClusterService clusterService;
85 private DeviceService deviceService;
86 private LinkService linkService;
87 private HostService hostService;
88 private MastershipService mastershipService;
89 private IntentService intentService;
90 private FlowRuleService flowService;
91 private StatisticService flowStatsService;
92 private PortStatisticsService portStatsService;
93 private TopologyService topologyService;
94 private TunnelService tunnelService;
95
96
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070097 // NOTE: we'll stick this here for now, but maybe there is a better home?
98 // (this is not distributed across the cluster)
99 private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
100
101
Simon Huntd5b96732016-07-08 13:22:27 -0700102 /**
103 * Creates an instance with a reference to the services directory, so that
104 * additional information about network elements may be looked up on
105 * on the fly.
106 *
107 * @param directory service directory
108 */
109 Topo2Jsonifier(ServiceDirectory directory) {
110 this.directory = checkNotNull(directory, "Directory cannot be null");
111
112 clusterService = directory.get(ClusterService.class);
113 deviceService = directory.get(DeviceService.class);
114 linkService = directory.get(LinkService.class);
115 hostService = directory.get(HostService.class);
116 mastershipService = directory.get(MastershipService.class);
117 intentService = directory.get(IntentService.class);
118 flowService = directory.get(FlowRuleService.class);
119 flowStatsService = directory.get(StatisticService.class);
120 portStatsService = directory.get(PortStatisticsService.class);
121 topologyService = directory.get(TopologyService.class);
122 tunnelService = directory.get(TunnelService.class);
Simon Hunt977aa052016-07-20 17:08:29 -0700123 }
Simon Huntd5b96732016-07-08 13:22:27 -0700124
Simon Hunt977aa052016-07-20 17:08:29 -0700125 // for unit testing
126 Topo2Jsonifier() {
Simon Huntd5b96732016-07-08 13:22:27 -0700127 }
128
129 private ObjectNode objectNode() {
130 return mapper.createObjectNode();
131 }
132
133 private ArrayNode arrayNode() {
134 return mapper.createArrayNode();
135 }
136
137 private String nullIsEmpty(Object o) {
138 return o == null ? "" : o.toString();
139 }
140
141
142 /**
143 * Returns a JSON representation of the cluster members (ONOS instances).
144 *
145 * @param instances the instance model objects
146 * @return a JSON representation of the data
147 */
148 ObjectNode instances(List<UiClusterMember> instances) {
149 NodeId local = clusterService.getLocalNode().id();
150 ObjectNode payload = objectNode();
151
152 ArrayNode members = arrayNode();
153 payload.set("members", members);
154 for (UiClusterMember member : instances) {
155 members.add(json(member, member.id().equals(local)));
156 }
157
158 return payload;
159 }
160
161 private ObjectNode json(UiClusterMember member, boolean isUiAttached) {
162 return objectNode()
163 .put("id", member.id().toString())
164 .put("ip", member.ip().toString())
165 .put("online", member.isOnline())
166 .put("ready", member.isReady())
167 .put("uiAttached", isUiAttached)
168 .put("switches", member.deviceCount());
169 }
170
171 /**
172 * Returns a JSON representation of the layout to use for displaying in
Simon Huntf836a872016-08-10 17:37:36 -0700173 * the topology view. The identifiers and names of regions from the
174 * current to the root is included, so that the bread-crumb widget can
175 * be rendered.
Simon Huntd5b96732016-07-08 13:22:27 -0700176 *
177 * @param layout the layout to transform
Simon Huntf836a872016-08-10 17:37:36 -0700178 * @param crumbs list of layouts in bread-crumb order
Simon Huntd5b96732016-07-08 13:22:27 -0700179 * @return a JSON representation of the data
180 */
Simon Huntf836a872016-08-10 17:37:36 -0700181 ObjectNode layout(UiTopoLayout layout, List<UiTopoLayout> crumbs) {
182 ObjectNode result = objectNode()
Simon Huntd5b96732016-07-08 13:22:27 -0700183 .put("id", layout.id().toString())
184 .put("parent", nullIsEmpty(layout.parent()))
185 .put("region", nullIsEmpty(layout.regionId()))
Simon Huntf836a872016-08-10 17:37:36 -0700186 .put("regionName", UiRegion.safeName(layout.region()));
187 addCrumbs(result, crumbs);
188 return result;
Simon Huntd5b96732016-07-08 13:22:27 -0700189 }
190
Simon Huntf836a872016-08-10 17:37:36 -0700191 private void addCrumbs(ObjectNode result, List<UiTopoLayout> crumbs) {
192 ArrayNode trail = arrayNode();
193 crumbs.forEach(c -> {
194 ObjectNode n = objectNode()
195 .put("id", c.regionId().toString())
196 .put("name", UiRegion.safeName(c.region()));
197 trail.add(n);
198 });
199 result.set("crumbs", trail);
Simon Huntd5b96732016-07-08 13:22:27 -0700200 }
201
202 /**
203 * Returns a JSON representation of the region to display in the topology
204 * view.
205 *
Simon Hunt977aa052016-07-20 17:08:29 -0700206 * @param region the region to transform to JSON
207 * @param subRegions the subregions within this region
Simon Huntc13082f2016-08-03 21:20:23 -0700208 * @param links the links within this region
Simon Huntd5b96732016-07-08 13:22:27 -0700209 * @return a JSON representation of the data
210 */
Simon Huntc13082f2016-08-03 21:20:23 -0700211 ObjectNode region(UiRegion region, Set<UiRegion> subRegions,
212 List<UiSynthLink> links) {
Simon Huntd5b96732016-07-08 13:22:27 -0700213 ObjectNode payload = objectNode();
Simon Huntd5b96732016-07-08 13:22:27 -0700214 if (region == null) {
215 payload.put("note", "no-region");
216 return payload;
217 }
Simon Hunt977aa052016-07-20 17:08:29 -0700218 payload.put("id", region.idAsString());
Simon Huntb1ce2602016-07-23 14:04:31 -0700219 if (subRegions != null) {
220 payload.set("subregions", jsonSubRegions(subRegions));
221 }
Simon Huntd5b96732016-07-08 13:22:27 -0700222
Simon Huntc13082f2016-08-03 21:20:23 -0700223 if (links != null) {
224 payload.set("links", jsonLinks(links));
225 }
226
Simon Hunt977aa052016-07-20 17:08:29 -0700227 List<String> layerTags = region.layerOrder();
228 List<Set<UiNode>> splitDevices = splitByLayer(layerTags, region.devices());
229 List<Set<UiNode>> splitHosts = splitByLayer(layerTags, region.hosts());
Simon Huntd5b96732016-07-08 13:22:27 -0700230
Simon Hunt977aa052016-07-20 17:08:29 -0700231 payload.set("devices", jsonGrouped(splitDevices));
232 payload.set("hosts", jsonGrouped(splitHosts));
Simon Hunt977aa052016-07-20 17:08:29 -0700233 payload.set("layerOrder", jsonStrings(layerTags));
Simon Huntd5b96732016-07-08 13:22:27 -0700234
235 return payload;
236 }
237
Simon Hunt977aa052016-07-20 17:08:29 -0700238 private ArrayNode jsonSubRegions(Set<UiRegion> subregions) {
239 ArrayNode kids = arrayNode();
Simon Huntc13082f2016-08-03 21:20:23 -0700240 subregions.forEach(s -> kids.add(jsonClosedRegion(s)));
Simon Hunt977aa052016-07-20 17:08:29 -0700241 return kids;
242 }
243
Simon Huntc13082f2016-08-03 21:20:23 -0700244 private JsonNode jsonLinks(List<UiSynthLink> links) {
245 ArrayNode synthLinks = arrayNode();
246 links.forEach(l -> synthLinks.add(json(l)));
247 return synthLinks;
248 }
249
Simon Hunt977aa052016-07-20 17:08:29 -0700250 private ArrayNode jsonStrings(List<String> strings) {
251 ArrayNode array = arrayNode();
252 strings.forEach(array::add);
253 return array;
254 }
255
Simon Hunt977aa052016-07-20 17:08:29 -0700256 private ArrayNode jsonGrouped(List<Set<UiNode>> groupedNodes) {
257 ArrayNode result = arrayNode();
258 groupedNodes.forEach(g -> {
259 ArrayNode subset = arrayNode();
260 g.forEach(n -> subset.add(json(n)));
261 result.add(subset);
262 });
263 return result;
264 }
265
Simon Hunt977aa052016-07-20 17:08:29 -0700266
267 private ObjectNode json(UiNode node) {
268 if (node instanceof UiRegion) {
269 return jsonClosedRegion((UiRegion) node);
270 }
271 if (node instanceof UiDevice) {
272 return json((UiDevice) node);
273 }
274 if (node instanceof UiHost) {
275 return json((UiHost) node);
276 }
277 throw new IllegalStateException(E_UNKNOWN_UI_NODE + node.getClass());
278 }
279
Simon Huntd5b96732016-07-08 13:22:27 -0700280 private ObjectNode json(UiDevice device) {
281 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700282 .put("id", device.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700283 .put("nodeType", DEVICE)
Simon Huntd5b96732016-07-08 13:22:27 -0700284 .put("type", device.type())
Simon Hunt3d712522016-08-11 11:20:44 -0700285 .put("online", deviceService.isAvailable(device.id()))
Simon Huntb1ce2602016-07-23 14:04:31 -0700286 .put("master", nullIsEmpty(device.master()))
Simon Huntd5b96732016-07-08 13:22:27 -0700287 .put("layer", device.layer());
288
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700289 Device d = device.backingDevice();
290
291 addProps(node, d);
292 addGeoLocation(node, d);
293 addMetaUi(node, device.idAsString());
Simon Huntd5b96732016-07-08 13:22:27 -0700294
295 return node;
296 }
297
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700298 private void addProps(ObjectNode node, Device dev) {
299 Annotations annot = dev.annotations();
300 ObjectNode props = objectNode();
301 if (annot != null) {
302 annot.keys().forEach(k -> props.put(k, annot.value(k)));
303 }
304 node.set("props", props);
305 }
Simon Huntd5b96732016-07-08 13:22:27 -0700306
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700307 private void addMetaUi(ObjectNode node, String metaInstanceId) {
308 ObjectNode meta = metaUi.get(metaInstanceId);
309 if (meta != null) {
310 node.set("metaUi", meta);
311 }
312 }
313
314 private void addGeoLocation(ObjectNode node, Annotated a) {
315 List<String> lngLat = getAnnotValues(a, LONGITUDE, LATITUDE);
316 if (lngLat != null) {
317 try {
318 double lng = Double.parseDouble(lngLat.get(0));
319 double lat = Double.parseDouble(lngLat.get(1));
320 ObjectNode loc = objectNode()
321 .put("type", "lnglat")
322 .put("lng", lng)
323 .put("lat", lat);
324 node.set("location", loc);
325
326 } catch (NumberFormatException e) {
327 log.warn("Invalid geo data: longitude={}, latitude={}",
328 lngLat.get(0), lngLat.get(1));
329 }
330 } else {
331 log.debug("No geo lng/lat for {}", a);
332 }
333 }
334
335 // return list of string values from annotated instance, for given keys
336 // return null if any keys are not present
337 List<String> getAnnotValues(Annotated a, String... annotKeys) {
338 List<String> result = new ArrayList<>(annotKeys.length);
339 for (String k : annotKeys) {
340 String v = a.annotations().value(k);
341 if (v == null) {
342 return null;
343 }
344 result.add(v);
345 }
346 return result;
347 }
348
349 // derive JSON object from annotations
350 private ObjectNode props(Annotations annotations) {
351 ObjectNode p = objectNode();
352 if (annotations != null) {
353 annotations.keys().forEach(k -> p.put(k, annotations.value(k)));
354 }
355 return p;
Simon Huntd5b96732016-07-08 13:22:27 -0700356 }
357
358 private ObjectNode json(UiHost host) {
359 return objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700360 .put("id", host.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700361 .put("nodeType", HOST)
Simon Huntd5b96732016-07-08 13:22:27 -0700362 .put("layer", host.layer());
363 // TODO: complete host details
364 }
365
Simon Huntc13082f2016-08-03 21:20:23 -0700366 private ObjectNode json(UiSynthLink sLink) {
367 UiLink uLink = sLink.link();
Simon Hunt3d712522016-08-11 11:20:44 -0700368 ObjectNode data = objectNode()
Simon Huntc13082f2016-08-03 21:20:23 -0700369 .put("id", uLink.idAsString())
370 .put("epA", uLink.endPointA())
371 .put("epB", uLink.endPointB())
372 .put("type", uLink.type());
Simon Hunt3d712522016-08-11 11:20:44 -0700373 String pA = uLink.endPortA();
374 String pB = uLink.endPortB();
375 if (pA != null) {
376 data.put("portA", pA);
377 }
378 if (pB != null) {
379 data.put("portB", pB);
380 }
381 return data;
Simon Huntd5b96732016-07-08 13:22:27 -0700382 }
383
384
Simon Hunt977aa052016-07-20 17:08:29 -0700385 private ObjectNode jsonClosedRegion(UiRegion region) {
386 return objectNode()
Simon Huntb1ce2602016-07-23 14:04:31 -0700387 .put("id", region.idAsString())
Simon Huntf836a872016-08-10 17:37:36 -0700388 .put("name", region.name())
Simon Hunt98189192016-07-29 19:02:27 -0700389 .put("nodeType", REGION)
Simon Huntb1ce2602016-07-23 14:04:31 -0700390 .put("nDevs", region.deviceCount());
Simon Hunt977aa052016-07-20 17:08:29 -0700391 // TODO: complete closed-region details
392 }
393
Simon Hunt98189192016-07-29 19:02:27 -0700394 /**
395 * Returns a JSON array representation of a set of regions/devices. Note
396 * that the information is sufficient for showing regions as nodes.
397 *
398 * @param nodes the nodes
399 * @return a JSON representation of the nodes
400 */
401 public ArrayNode closedNodes(Set<UiNode> nodes) {
402 ArrayNode array = arrayNode();
Simon Huntc13082f2016-08-03 21:20:23 -0700403 for (UiNode node : nodes) {
Simon Hunt98189192016-07-29 19:02:27 -0700404 if (node instanceof UiRegion) {
405 array.add(jsonClosedRegion((UiRegion) node));
406 } else if (node instanceof UiDevice) {
407 array.add(json((UiDevice) node));
408 } else {
409 log.warn("Unexpected node instance: {}", node.getClass());
410 }
411 }
412 return array;
413 }
Simon Hunt977aa052016-07-20 17:08:29 -0700414
415 /**
416 * Returns a JSON array representation of a list of regions. Note that the
417 * information about each region is limited to what needs to be used to
418 * show the regions as nodes on the view.
419 *
420 * @param regions the regions
421 * @return a JSON representation of the minimal region information
422 */
423 public ArrayNode closedRegions(Set<UiRegion> regions) {
424 ArrayNode array = arrayNode();
425 for (UiRegion r : regions) {
426 array.add(jsonClosedRegion(r));
427 }
428 return array;
429 }
430
431 /**
432 * Returns a JSON array representation of a list of devices.
433 *
434 * @param devices the devices
435 * @return a JSON representation of the devices
436 */
437 public ArrayNode devices(Set<UiDevice> devices) {
438 ArrayNode array = arrayNode();
439 for (UiDevice device : devices) {
440 array.add(json(device));
441 }
442 return array;
443 }
444
445 /**
446 * Returns a JSON array representation of a list of hosts.
447 *
448 * @param hosts the hosts
449 * @return a JSON representation of the hosts
450 */
451 public ArrayNode hosts(Set<UiHost> hosts) {
452 ArrayNode array = arrayNode();
453 for (UiHost host : hosts) {
454 array.add(json(host));
455 }
456 return array;
457 }
458
Simon Hunt977aa052016-07-20 17:08:29 -0700459 // package-private for unit testing
460 List<Set<UiNode>> splitByLayer(List<String> layerTags,
461 Set<? extends UiNode> nodes) {
462 final int nLayers = layerTags.size();
463 if (!layerTags.get(nLayers - 1).equals(LAYER_DEFAULT)) {
464 throw new IllegalArgumentException(E_DEF_NOT_LAST);
465 }
466
467 List<Set<UiNode>> splitList = new ArrayList<>(layerTags.size());
468 Map<String, Set<UiNode>> byLayer = new HashMap<>(layerTags.size());
469
470 for (String tag : layerTags) {
471 Set<UiNode> set = new HashSet<>();
472 byLayer.put(tag, set);
473 splitList.add(set);
474 }
475
476 for (UiNode n : nodes) {
477 String which = n.layer();
478 if (!layerTags.contains(which)) {
479 which = LAYER_DEFAULT;
480 }
481 byLayer.get(which).add(n);
482 }
483
484 return splitList;
485 }
Steven Burrowse7cc3082016-09-27 11:24:58 -0700486
487 /**
488 * Stores the memento for an element.
489 * This method assumes the payload has an id String, memento ObjectNode
490 *
491 * @param payload event payload
492 */
493 void updateMeta(ObjectNode payload) {
494
495 String id = JsonUtils.string(payload, "id");
496 metaUi.put(id, JsonUtils.node(payload, "memento"));
497
498 log.debug("Storing metadata for {}", id);
499 }
Simon Huntd5b96732016-07-08 13:22:27 -0700500}