blob: 8bb6244f2c9655f2636422792ecb7daace3c981c [file] [log] [blame]
Thomas Vachuska3553b302015-03-07 14:49:43 -08001/*
Brian O'Connor5ab426f2016-04-09 01:19:45 -07002 * Copyright 2015-present Open Networking Laboratory
Thomas Vachuska3553b302015-03-07 14:49:43 -08003 *
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.ObjectMapper;
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -070019import com.fasterxml.jackson.databind.node.ArrayNode;
Thomas Vachuska3553b302015-03-07 14:49:43 -080020import com.fasterxml.jackson.databind.node.ObjectNode;
21import org.eclipse.jetty.websocket.WebSocket;
22import org.onlab.osgi.ServiceDirectory;
Thomas Vachuskafc52fec2015-05-18 19:13:56 -070023import org.onlab.osgi.ServiceNotFoundException;
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -070024import org.onosproject.cluster.ClusterService;
25import org.onosproject.cluster.ControllerNode;
Simon Hunt8add9ee2016-09-20 17:05:07 -070026import org.onosproject.ui.GlyphConstants;
Thomas Vachuska3553b302015-03-07 14:49:43 -080027import org.onosproject.ui.UiConnection;
Simon Hunt2d7cd6f2017-05-04 13:04:50 -070028import org.onosproject.ui.UiExtension;
Thomas Vachuska3553b302015-03-07 14:49:43 -080029import org.onosproject.ui.UiExtensionService;
Simon Hunta0ddb022015-05-01 09:53:01 -070030import org.onosproject.ui.UiMessageHandler;
Simon Hunt7715e892016-04-12 19:55:32 -070031import org.onosproject.ui.UiMessageHandlerFactory;
Simon Hunt22c35df2017-04-26 17:28:42 -070032import org.onosproject.ui.UiTopo2OverlayFactory;
Thomas Vachuska92b016b2016-05-20 11:37:57 -070033import org.onosproject.ui.UiTopoLayoutService;
Simon Hunte05cae42015-07-23 17:35:24 -070034import org.onosproject.ui.UiTopoOverlayFactory;
Simon Hunt537bc762016-12-20 12:15:13 -080035import org.onosproject.ui.impl.topo.Topo2Jsonifier;
Simon Hunt22c35df2017-04-26 17:28:42 -070036import org.onosproject.ui.impl.topo.Topo2OverlayCache;
Simon Hunt2d7cd6f2017-05-04 13:04:50 -070037import org.onosproject.ui.impl.topo.Topo2TrafficMessageHandler;
Simon Hunt22c35df2017-04-26 17:28:42 -070038import org.onosproject.ui.impl.topo.Topo2ViewMessageHandler;
Thomas Vachuska92b016b2016-05-20 11:37:57 -070039import org.onosproject.ui.impl.topo.UiTopoSession;
40import org.onosproject.ui.impl.topo.model.UiSharedTopologyModel;
41import org.onosproject.ui.model.topo.UiTopoLayout;
Thomas Vachuska3553b302015-03-07 14:49:43 -080042import org.slf4j.Logger;
43import org.slf4j.LoggerFactory;
44
45import java.io.IOException;
46import java.util.HashMap;
47import java.util.Map;
48
49/**
Simon Hunt7092cc42016-04-06 18:40:17 -070050 * Web socket capable of interacting with the Web UI.
Thomas Vachuska3553b302015-03-07 14:49:43 -080051 */
52public class UiWebSocket
53 implements UiConnection, WebSocket.OnTextMessage, WebSocket.OnControl {
54
55 private static final Logger log = LoggerFactory.getLogger(UiWebSocket.class);
56
Simon Hunt7715e892016-04-12 19:55:32 -070057 private static final String EVENT = "event";
Simon Hunt7715e892016-04-12 19:55:32 -070058 private static final String PAYLOAD = "payload";
59 private static final String UNKNOWN = "unknown";
60
61 private static final String ID = "id";
62 private static final String IP = "ip";
63 private static final String CLUSTER_NODES = "clusterNodes";
64 private static final String USER = "user";
65 private static final String BOOTSTRAP = "bootstrap";
66
Simon Huntd7395c82016-10-20 17:54:01 -070067 private static final String TOPO = "topo";
Thomas Vachuska92b016b2016-05-20 11:37:57 -070068
Thomas Vachuska1a989c12015-06-09 18:29:22 -070069 private static final long MAX_AGE_MS = 30_000;
Thomas Vachuska3553b302015-03-07 14:49:43 -080070
71 private static final byte PING = 0x9;
72 private static final byte PONG = 0xA;
73 private static final byte[] PING_DATA = new byte[]{(byte) 0xde, (byte) 0xad};
74
Simon Hunt7092cc42016-04-06 18:40:17 -070075 private final ObjectMapper mapper = new ObjectMapper();
Thomas Vachuska3553b302015-03-07 14:49:43 -080076 private final ServiceDirectory directory;
Thomas Vachuska92b016b2016-05-20 11:37:57 -070077 private final UiTopoSession topoSession;
Thomas Vachuska3553b302015-03-07 14:49:43 -080078
79 private Connection connection;
80 private FrameConnection control;
Thomas Vachuska0af26912016-03-21 21:37:30 -070081 private String userName;
Thomas Vachuska92b016b2016-05-20 11:37:57 -070082 private String currentView;
Thomas Vachuska3553b302015-03-07 14:49:43 -080083
Thomas Vachuska3553b302015-03-07 14:49:43 -080084 private long lastActive = System.currentTimeMillis();
85
Simon Hunta0ddb022015-05-01 09:53:01 -070086 private Map<String, UiMessageHandler> handlers;
Simon Hunte05cae42015-07-23 17:35:24 -070087 private TopoOverlayCache overlayCache;
Simon Hunt22c35df2017-04-26 17:28:42 -070088 private Topo2OverlayCache overlay2Cache;
Thomas Vachuska3553b302015-03-07 14:49:43 -080089
90 /**
Simon Huntcda9c032016-04-11 10:32:54 -070091 * Creates a new web-socket for serving data to the Web UI.
Thomas Vachuska3553b302015-03-07 14:49:43 -080092 *
93 * @param directory service directory
Simon Hunt7715e892016-04-12 19:55:32 -070094 * @param userName user name of the logged-in user
Thomas Vachuska3553b302015-03-07 14:49:43 -080095 */
Thomas Vachuska0af26912016-03-21 21:37:30 -070096 public UiWebSocket(ServiceDirectory directory, String userName) {
Thomas Vachuska3553b302015-03-07 14:49:43 -080097 this.directory = directory;
Thomas Vachuska0af26912016-03-21 21:37:30 -070098 this.userName = userName;
Simon Hunt537bc762016-12-20 12:15:13 -080099
Simon Hunt95f4b422017-03-03 13:49:05 -0800100 Topo2Jsonifier t2json = new Topo2Jsonifier(directory, userName);
Simon Hunt537bc762016-12-20 12:15:13 -0800101 UiSharedTopologyModel sharedModel = directory.get(UiSharedTopologyModel.class);
102 UiTopoLayoutService layoutService = directory.get(UiTopoLayoutService.class);
Simon Huntbbd0f462017-01-10 14:50:22 -0800103
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800104 sharedModel.injectJsonifier(t2json);
105
Simon Huntbbd0f462017-01-10 14:50:22 -0800106 topoSession = new UiTopoSession(this, t2json, sharedModel, layoutService);
Thomas Vachuska0af26912016-03-21 21:37:30 -0700107 }
108
109 @Override
110 public String userName() {
111 return userName;
Thomas Vachuska3553b302015-03-07 14:49:43 -0800112 }
113
Thomas Vachuska92b016b2016-05-20 11:37:57 -0700114 @Override
115 public UiTopoLayout currentLayout() {
116 return topoSession.currentLayout();
117 }
118
119 @Override
120 public void setCurrentLayout(UiTopoLayout topoLayout) {
121 topoSession.setCurrentLayout(topoLayout);
122 }
123
124 @Override
125 public String currentView() {
126 return currentView;
127 }
128
129 @Override
130 public void setCurrentView(String viewId) {
131 currentView = viewId;
132 topoSession.enableEvent(viewId.equals(TOPO));
133 }
134
Thomas Vachuska3553b302015-03-07 14:49:43 -0800135 /**
Simon Huntd5b96732016-07-08 13:22:27 -0700136 * Provides a reference to the topology session.
137 *
138 * @return topo session reference
139 */
140 public UiTopoSession topoSession() {
141 return topoSession;
142 }
143
144 /**
Thomas Vachuska3553b302015-03-07 14:49:43 -0800145 * Issues a close on the connection.
146 */
147 synchronized void close() {
Simon Hunte05cae42015-07-23 17:35:24 -0700148 destroyHandlersAndOverlays();
Thomas Vachuska3553b302015-03-07 14:49:43 -0800149 if (connection.isOpen()) {
150 connection.close();
151 }
152 }
153
154 /**
155 * Indicates if this connection is idle.
156 *
157 * @return true if idle or closed
158 */
159 synchronized boolean isIdle() {
Simon Huntda580882015-05-12 20:58:18 -0700160 long quietFor = System.currentTimeMillis() - lastActive;
161 boolean idle = quietFor > MAX_AGE_MS;
Thomas Vachuska3553b302015-03-07 14:49:43 -0800162 if (idle || (connection != null && !connection.isOpen())) {
Simon Huntda580882015-05-12 20:58:18 -0700163 log.debug("IDLE (or closed) websocket [{} ms]", quietFor);
Thomas Vachuska3553b302015-03-07 14:49:43 -0800164 return true;
165 } else if (connection != null) {
166 try {
167 control.sendControl(PING, PING_DATA, 0, PING_DATA.length);
168 } catch (IOException e) {
169 log.warn("Unable to send ping message due to: ", e);
170 }
171 }
172 return false;
173 }
174
175 @Override
Satish K598c28d2015-11-24 17:20:40 +0530176 public synchronized void onOpen(Connection connection) {
Thomas Vachuska3553b302015-03-07 14:49:43 -0800177 this.connection = connection;
178 this.control = (FrameConnection) connection;
Thomas Vachuskafc52fec2015-05-18 19:13:56 -0700179 try {
Thomas Vachuska92b016b2016-05-20 11:37:57 -0700180 topoSession.init();
Simon Hunte05cae42015-07-23 17:35:24 -0700181 createHandlersAndOverlays();
Simon Hunt7715e892016-04-12 19:55:32 -0700182 sendBootstrapData();
183 log.info("GUI client connected -- user <{}>", userName);
Thomas Vachuskafc52fec2015-05-18 19:13:56 -0700184
185 } catch (ServiceNotFoundException e) {
Brian O'Connor75deea62015-06-24 16:09:17 -0400186 log.warn("Unable to open GUI connection; services have been shut-down", e);
Thomas Vachuskafc52fec2015-05-18 19:13:56 -0700187 this.connection.close();
188 this.connection = null;
189 this.control = null;
190 }
Thomas Vachuska3553b302015-03-07 14:49:43 -0800191 }
192
193 @Override
194 public synchronized void onClose(int closeCode, String message) {
Thomas Vachuska92b016b2016-05-20 11:37:57 -0700195 topoSession.destroy();
Simon Hunte05cae42015-07-23 17:35:24 -0700196 destroyHandlersAndOverlays();
Simon Huntda580882015-05-12 20:58:18 -0700197 log.info("GUI client disconnected [close-code={}, message={}]",
Simon Hunt22c35df2017-04-26 17:28:42 -0700198 closeCode, message);
Thomas Vachuska3553b302015-03-07 14:49:43 -0800199 }
200
201 @Override
202 public boolean onControl(byte controlCode, byte[] data, int offset, int length) {
203 lastActive = System.currentTimeMillis();
204 return true;
205 }
206
207 @Override
208 public void onMessage(String data) {
209 lastActive = System.currentTimeMillis();
210 try {
211 ObjectNode message = (ObjectNode) mapper.reader().readTree(data);
Simon Hunt7715e892016-04-12 19:55:32 -0700212 String type = message.path(EVENT).asText(UNKNOWN);
Simon Hunta0ddb022015-05-01 09:53:01 -0700213 UiMessageHandler handler = handlers.get(type);
Thomas Vachuska3553b302015-03-07 14:49:43 -0800214 if (handler != null) {
Simon Huntd5b96732016-07-08 13:22:27 -0700215 log.debug("RX message: {}", message);
Thomas Vachuska3553b302015-03-07 14:49:43 -0800216 handler.process(message);
217 } else {
218 log.warn("No GUI message handler for type {}", type);
219 }
220 } catch (Exception e) {
221 log.warn("Unable to parse GUI message {} due to {}", data, e);
222 log.debug("Boom!!!", e);
223 }
224 }
225
226 @Override
Thomas Vachuska35fa3d42015-04-30 10:11:47 -0700227 public synchronized void sendMessage(ObjectNode message) {
Thomas Vachuska3553b302015-03-07 14:49:43 -0800228 try {
229 if (connection.isOpen()) {
230 connection.sendMessage(message.toString());
Simon Huntd5b96732016-07-08 13:22:27 -0700231 log.debug("TX message: {}", message);
Thomas Vachuska3553b302015-03-07 14:49:43 -0800232 }
233 } catch (IOException e) {
234 log.warn("Unable to send message {} to GUI due to {}", message, e);
235 log.debug("Boom!!!", e);
236 }
237 }
238
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700239 @Override
Simon Huntc5368182017-01-10 13:32:04 -0800240 public synchronized void sendMessage(String type, ObjectNode payload) {
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700241 ObjectNode message = mapper.createObjectNode();
Simon Hunt7715e892016-04-12 19:55:32 -0700242 message.put(EVENT, type);
Simon Hunt7715e892016-04-12 19:55:32 -0700243 message.set(PAYLOAD, payload != null ? payload : mapper.createObjectNode());
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700244 sendMessage(message);
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700245 }
246
Thomas Vachuska3553b302015-03-07 14:49:43 -0800247 // Creates new message handlers.
Simon Hunte05cae42015-07-23 17:35:24 -0700248 private synchronized void createHandlersAndOverlays() {
Simon Hunt7715e892016-04-12 19:55:32 -0700249 log.debug("Creating handlers and overlays...");
Thomas Vachuska3553b302015-03-07 14:49:43 -0800250 handlers = new HashMap<>();
Simon Hunte05cae42015-07-23 17:35:24 -0700251 overlayCache = new TopoOverlayCache();
Simon Hunt22c35df2017-04-26 17:28:42 -0700252 overlay2Cache = new Topo2OverlayCache();
Simon Hunte05cae42015-07-23 17:35:24 -0700253
Simon Hunt2d7cd6f2017-05-04 13:04:50 -0700254 Map<Class<?>, UiMessageHandler> handlerInstances = new HashMap<>();
255
Thomas Vachuska3553b302015-03-07 14:49:43 -0800256 UiExtensionService service = directory.get(UiExtensionService.class);
Thomas Vachuska329af532015-03-10 02:08:33 -0700257 service.getExtensions().forEach(ext -> {
258 UiMessageHandlerFactory factory = ext.messageHandlerFactory();
259 if (factory != null) {
260 factory.newHandlers().forEach(handler -> {
Thomas Vachuskac4178cc2015-12-10 11:43:32 -0800261 try {
262 handler.init(this, directory);
263 handler.messageTypes().forEach(type -> handlers.put(type, handler));
Simon Hunt2d7cd6f2017-05-04 13:04:50 -0700264 handlerInstances.put(handler.getClass(), handler);
Simon Hunte05cae42015-07-23 17:35:24 -0700265
Thomas Vachuskac4178cc2015-12-10 11:43:32 -0800266 } catch (Exception e) {
267 log.warn("Unable to setup handler {} due to", handler, e);
Simon Hunte05cae42015-07-23 17:35:24 -0700268 }
Thomas Vachuska329af532015-03-10 02:08:33 -0700269 });
270 }
Simon Hunte05cae42015-07-23 17:35:24 -0700271
Simon Hunt2d7cd6f2017-05-04 13:04:50 -0700272 registerOverlays(ext);
Thomas Vachuska329af532015-03-10 02:08:33 -0700273 });
Simon Hunt2d7cd6f2017-05-04 13:04:50 -0700274
275 handlerCrossConnects(handlerInstances);
276
277 log.debug("#handlers = {}, #overlays = {}", handlers.size(), overlayCache.size());
278 }
279
280 private void registerOverlays(UiExtension ext) {
281 UiTopoOverlayFactory overlayFactory = ext.topoOverlayFactory();
282 if (overlayFactory != null) {
283 overlayFactory.newOverlays().forEach(overlayCache::add);
284 }
285
286 UiTopo2OverlayFactory overlay2Factory = ext.topo2OverlayFactory();
287 if (overlay2Factory != null) {
288 overlay2Factory.newOverlays().forEach(overlay2Cache::add);
289 }
290 }
291
292 private void handlerCrossConnects(Map<Class<?>, UiMessageHandler> handlers) {
293 TopologyViewMessageHandler topomh = (TopologyViewMessageHandler)
294 handlers.get(TopologyViewMessageHandler.class);
295 if (topomh != null) {
296 topomh.setOverlayCache(overlayCache);
297 }
298
299 Topo2ViewMessageHandler topo2mh = (Topo2ViewMessageHandler)
300 handlers.get(Topo2ViewMessageHandler.class);
301 if (topo2mh != null) {
302 topo2mh.setOverlayCache(overlay2Cache);
303
304 // We also need a link to Topo2Traffic
305 Topo2TrafficMessageHandler topo2traffic = (Topo2TrafficMessageHandler)
306 handlers.get(Topo2TrafficMessageHandler.class);
307 if (topo2traffic != null) {
308 topo2mh.setTrafficHandler(topo2traffic);
309 } else {
310 log.error("No topo2 traffic handler found");
311 }
312 }
Thomas Vachuska3553b302015-03-07 14:49:43 -0800313 }
314
315 // Destroys message handlers.
Simon Hunte05cae42015-07-23 17:35:24 -0700316 private synchronized void destroyHandlersAndOverlays() {
Simon Hunt7715e892016-04-12 19:55:32 -0700317 log.debug("Destroying handlers and overlays...");
Thomas Vachuska3553b302015-03-07 14:49:43 -0800318 handlers.forEach((type, handler) -> handler.destroy());
319 handlers.clear();
Simon Hunte05cae42015-07-23 17:35:24 -0700320
321 if (overlayCache != null) {
322 overlayCache.destroy();
323 overlayCache = null;
324 }
Simon Hunte6f64612017-04-28 00:01:48 -0700325 if (overlay2Cache != null) {
326 overlay2Cache.destroy();
327 overlay2Cache = null;
328 }
Thomas Vachuska3553b302015-03-07 14:49:43 -0800329 }
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700330
Simon Hunt7715e892016-04-12 19:55:32 -0700331 // Sends initial information (username and cluster member information)
332 // to allow GUI to display logged-in user, and to be able to
333 // fail-over to an alternate cluster member if necessary.
334 private void sendBootstrapData() {
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700335 ClusterService service = directory.get(ClusterService.class);
336 ArrayNode instances = mapper.createArrayNode();
337
338 for (ControllerNode node : service.getNodes()) {
339 ObjectNode instance = mapper.createObjectNode()
Simon Hunt7715e892016-04-12 19:55:32 -0700340 .put(ID, node.id().toString())
341 .put(IP, node.ip().toString())
Simon Hunt8add9ee2016-09-20 17:05:07 -0700342 .put(GlyphConstants.UI_ATTACHED,
Simon Hunt22c35df2017-04-26 17:28:42 -0700343 node.equals(service.getLocalNode()));
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700344 instances.add(instance);
345 }
346
347 ObjectNode payload = mapper.createObjectNode();
Simon Hunt7715e892016-04-12 19:55:32 -0700348 payload.set(CLUSTER_NODES, instances);
349 payload.put(USER, userName);
Simon Huntc5368182017-01-10 13:32:04 -0800350 sendMessage(BOOTSTRAP, payload);
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700351 }
352
Thomas Vachuska3553b302015-03-07 14:49:43 -0800353}