Simon Hunt

GUI -- Added updateDevice event handling.

 - Display offline devices as grey.
 - Tracing web socket messages (for now, in console; in future, to trace view).
 - Captured sample events for use with test scenarios - both from and to the server.
 - Added description to scenario file.

Change-Id: I7825b32d63496ebea2ab5789519fb0c6af6c5257
Showing 36 changed files with 572 additions and 82 deletions
......@@ -44,6 +44,7 @@
<!-- This is where contributed stylesheets get INJECTED -->
<!-- TODO: replace with template marker and inject refs server-side -->
<link rel="stylesheet" href="topo2.css">
<link rel="stylesheet" href="webSockTrace.css">
<!-- General library modules included here-->
......@@ -97,6 +98,7 @@
<!-- Contributed (application) views injected here -->
<!-- TODO: replace with template marker and inject refs server-side -->
<script src="webSockTrace.js"></script>
<script src="topo2.js"></script>
<!-- finally, build the UI-->
......
{
"event": "addDevice",
"payload": {
"id": "of:0000000000000003",
"type": "switch",
"online": true,
"labels": [
"of:0000000000000003",
"3",
"",
null
],
"props": {}
}
}
{
"event": "addHost",
"payload": {
"id": "6A:40:24:F7:9C:2C/-1",
"ingress": "6A:40:24:F7:9C:2C/-1/0-of:0000000000000003/2",
"egress": "of:0000000000000003/2-6A:40:24:F7:9C:2C/-1/0",
"cp": {
"device": "of:0000000000000003",
"port": 2
},
"labels": [
"unknown",
"6A:40:24:F7:9C:2C"
],
"props": {}
}
}
{
"event": "addLink",
"payload": {
"id": "of:0000000000000007/4-of:0000000000000006/1",
"type": "direct",
"linkWidth": 2,
"src": "of:0000000000000007",
"srcPort": "4",
"dst": "of:0000000000000006",
"dstPort": "1"
}
}
{
"__comments__": [
"fabricated event",
"not sure if this is the actual format",
"but we really only care about 'id' being in the payload"
],
"event": "removeDevice",
"payload": {
"id": "of:0000000000000002",
"type": "switch",
"online": true,
"labels": [
"of:0000000000000002",
"2",
"",
null
],
"props": {}
}
}
{
"__comments__": [
"fabricated event",
"not sure if this is the actual format",
"but we really only care about 'id' being in the payload"
],
"event": "removeHost",
"payload": {
"id": "6A:40:24:F7:9C:2C/-1",
"ingress": "6A:40:24:F7:9C:2C/-1/0-of:0000000000000003/2",
"egress": "of:0000000000000003/2-6A:40:24:F7:9C:2C/-1/0",
"cp": {
"device": "of:0000000000000003",
"port": 2
},
"labels": [
"unknown",
"6A:40:24:F7:9C:2C"
],
"props": {}
}
}
{
"event": "removeLink",
"payload": {
"id": "of:0000000000000001/1-of:0000000000000002/4",
"type": "direct",
"linkWidth": 2,
"src": "of:0000000000000001",
"srcPort": "1",
"dst": "of:0000000000000002",
"dstPort": "4"
}
}
{
"event": "showPath",
"sid": 15,
"payload": {
"links": [
"62:4F:65:BF:FF:B3/-1/0-of:000000000000000b/1",
"of:000000000000000b/4-of:000000000000000a/1",
"of:000000000000000a/4-of:0000000000000001/3",
"of:0000000000000001/1-of:0000000000000002/4",
"of:0000000000000002/1-of:0000000000000003/4",
"of:0000000000000003/1-CA:4B:EE:A4:B0:33/-1/0"
],
"intentId": "0x52a914f9"
}
}
{
"event": "updateDevice",
"payload": {
"id": "of:0000000000000002",
"type": "switch",
"online": true,
"labels": [
"of:0000000000000002",
"2",
"",
null
],
"props": {}
}
}
{
"event": "updateDevice",
"payload": {
"id": "of:0000000000000002",
"type": "switch",
"online": false,
"labels": [
"of:0000000000000002",
"2",
"",
null
],
"props": {}
}
}
{
"event": "updateHost",
"payload": {
"id": "AA:C2:74:3F:B8:06/-1",
"ingress": "AA:C2:74:3F:B8:06/-1/0-of:0000000000000005/3",
"egress": "of:0000000000000005/3-AA:C2:74:3F:B8:06/-1/0",
"cp": {
"device": "of:0000000000000005",
"port": 3
},
"labels": [
"10.0.0.9",
"AA:C2:74:3F:B8:06"
],
"props":{}
}
}
{
"event": "updateLink",
"payload": {
"id": "of:0000000000000002/4-of:0000000000000001/1",
"type": "direct",
"linkWidth": 2,
"src": "of:0000000000000002",
"srcPort": "4",
"dst": "of:0000000000000001",
"dstPort": "1"
}
}
{
"event": "requestPath",
"sid": 15,
"payload": {
"one": "62:4F:65:BF:FF:B3/-1",
"two": "CA:4B:EE:A4:B0:33/-1"
}
}
{
"event": "updateMeta",
"sid": 11,
"payload": {
"id": "62:4F:65:BF:FF:B3/-1",
"class": "host",
"x": 197,
"y": 177
}
}
{
"event": "addLink",
"event": "updateDevice",
"payload": {
"id": "of:0000ffffffff0003/21-of:0000ffffffff0008/20",
"type": "direct",
"linkWidth": 2,
"src": "of:0000ffffffff0003",
"srcPort": "21",
"dst": "of:0000ffffffff0008",
"dstPort": "20",
"props" : {
"BW": "70 G"
"id": "of:0000ffffffff0008",
"type": "switch",
"online": true,
"labels": [
"0000ffffffff0008",
"FF:FF:FF:FF:00:08",
"sw-8-yo",
""
],
"metaUi": {
"x": 400,
"y": 280
}
}
}
......
{
"event": "addHost",
"event": "updateDevice",
"payload": {
"id": "0E:2A:69:30:13:86/-1",
"ingress": "0E:2A:69:30:13:86/-1/0-of:0000ffffffff0003/2",
"egress": "of:0000ffffffff0003/2-0E:2A:69:30:13:86/-1/0",
"cp": {
"device": "of:0000ffffffff0003",
"port": 2
},
"id": "of:0000ffffffff0003",
"type": "switch",
"online": true,
"labels": [
"unknown",
"0E:2A:69:30:13:86"
"0000ffffffff0003",
"FF:FF:FF:FF:00:03",
"sw-3-yo",
""
],
"props": {}
"metaUi": {
"x": 800,
"y": 280
}
}
}
......
{
"event": "addHost",
"event": "addLink",
"payload": {
"id": "A6:96:E5:03:52:5F/-1",
"ingress": "A6:96:E5:03:52:5F/-1/0-of:0000ffffffff0008/1",
"egress": "of:0000ffffffff0008/1-A6:96:E5:03:52:5F/-1/0",
"cp": {
"device": "of:0000ffffffff0008",
"port": 1
},
"labels": [
"unknown",
"A6:96:E5:03:52:5F"
],
"props": {}
"id": "of:0000ffffffff0003/21-of:0000ffffffff0008/20",
"type": "direct",
"linkWidth": 2,
"src": "of:0000ffffffff0003",
"srcPort": "21",
"dst": "of:0000ffffffff0008",
"dstPort": "20",
"props" : {
"BW": "70 G"
}
}
}
......
{
"event": "updateHost",
"event": "addHost",
"payload": {
"id": "0E:2A:69:30:13:86/-1",
"ingress": "0E:2A:69:30:13:86/-1/0-of:0000ffffffff0003/2",
......@@ -9,7 +9,7 @@
"port": 2
},
"labels": [
"10.0.0.13",
"unknown",
"0E:2A:69:30:13:86"
],
"props": {}
......
{
"event": "updateHost",
"event": "addHost",
"payload": {
"id": "A6:96:E5:03:52:5F/-1",
"ingress": "A6:96:E5:03:52:5F/-1/0-of:0000ffffffff0008/1",
......@@ -9,7 +9,7 @@
"port": 1
},
"labels": [
"10.0.0.17",
"unknown",
"A6:96:E5:03:52:5F"
],
"props": {}
......
{
"event": "updateHost",
"payload": {
"id": "0E:2A:69:30:13:86/-1",
"ingress": "0E:2A:69:30:13:86/-1/0-of:0000ffffffff0003/2",
"egress": "of:0000ffffffff0003/2-0E:2A:69:30:13:86/-1/0",
"cp": {
"device": "of:0000ffffffff0003",
"port": 2
},
"labels": [
"10.0.0.13",
"0E:2A:69:30:13:86"
],
"props": {}
}
}
{
"event": "updateHost",
"payload": {
"id": "A6:96:E5:03:52:5F/-1",
"ingress": "A6:96:E5:03:52:5F/-1/0-of:0000ffffffff0008/1",
"egress": "of:0000ffffffff0008/1-A6:96:E5:03:52:5F/-1/0",
"cp": {
"device": "of:0000ffffffff0008",
"port": 1
},
"labels": [
"10.0.0.17",
"A6:96:E5:03:52:5F"
],
"props": {}
}
}
......@@ -6,5 +6,17 @@
"title": "Simple Startup Scenario",
"params": {
"lastAuto": 0
}
},
"description": [
"1. add device [8] (offline)",
"2. add device [3] (offline)",
"3. update device [8] (online)",
"4. update device [3] (online)",
"5. add link [3] --> [8]",
"6. add host (to [3])",
"7. add host (to [8])",
"8. update host[3] (IP now 10.0.0.13)",
"9. update host[8] (IP now 10.0.0.17)",
""
]
}
\ No newline at end of file
......
......@@ -33,7 +33,8 @@
var uiApi,
viewApi,
navApi,
libApi;
libApi,
exported = {};
var defaultOptions = {
trace: false,
......@@ -658,6 +659,7 @@
return makeUid(this, id);
},
// TODO : add exportApi and importApi methods
// TODO : implement custom dialogs
// Consider enhancing alert mechanism to handle multiples
......@@ -737,6 +739,7 @@
// ..........................................................
// View API
// TODO: deprecated
viewApi = {
/** @api view empty( )
* Empties the current view.
......@@ -802,7 +805,8 @@
lib: libApi,
//view: viewApi,
nav: navApi,
buildUi: buildOnosUi
buildUi: buildOnosUi,
exported: exported
};
};
......
......@@ -41,11 +41,16 @@
stroke: #ccc;
}
#topo svg .node.device.switch {
/* note: device is offline without the 'online' class */
#topo svg .node.device {
fill: #777;
}
#topo svg .node.device.switch.online {
fill: #17f;
}
#topo svg .node.device.roadm {
#topo svg .node.device.roadm.online {
fill: #03c;
}
......@@ -53,12 +58,17 @@
fill: #846;
}
/* note: device is offline without the 'online' class */
#topo svg .node.device text {
fill: white;
fill: #aaa;
font: 10pt sans-serif;
pointer-events: none;
}
#topo svg .node.device.online text {
fill: white;
}
#topo svg .node.host text {
fill: #846;
font: 9pt sans-serif;
......
......@@ -24,7 +24,8 @@
'use strict';
// shorter names for library APIs
var d3u = onos.lib.d3util;
var d3u = onos.lib.d3util,
trace;
// configuration data
var config = {
......@@ -241,8 +242,8 @@
}
function handleUiEvent(data) {
testDebug('handleUiEvent(): ' + data.event);
// TODO:
scenario.view.alert('UI Tx: ' + data.event + '\n\n' +
JSON.stringify(data));
}
function injectStartupEvents(view) {
......@@ -259,12 +260,7 @@
bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
}
function cycleLabels() {
deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1) ? 0 : deviceLabelIndex + 1;
network.nodes.forEach(function (d) {
if (d.class !== 'device') { return; }
function updateDeviceLabel(d) {
var label = niceLabel(deviceLabel(d)),
node = d.el,
box;
......@@ -285,6 +281,23 @@
.transition()
.attr('x', box.x + config.icons.xoff)
.attr('y', box.y + config.icons.yoff);
}
function updateHostLabel(d) {
var label = hostLabel(d),
host = d.el;
host.select('text').text(label);
}
function cycleLabels() {
deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1)
? 0 : deviceLabelIndex + 1;
network.nodes.forEach(function (d) {
if (d.class === 'device') {
updateDeviceLabel(d);
}
});
}
......@@ -348,15 +361,20 @@
// ==============================
// Event handlers for server-pushed events
function logicError(msg) {
// TODO, report logic error to server, via websock, so it can be logged
network.view.alert('Logic Error:\n\n' + msg);
}
var eventDispatch = {
addDevice: addDevice,
updateDevice: stillToImplement,
removeDevice: stillToImplement,
addLink: addLink,
updateLink: stillToImplement,
removeLink: stillToImplement,
addHost: addHost,
updateDevice: updateDevice,
updateLink: stillToImplement,
updateHost: updateHost,
removeDevice: stillToImplement,
removeLink: stillToImplement,
removeHost: stillToImplement,
showPath: showPath
};
......@@ -364,8 +382,6 @@
function addDevice(data) {
var device = data.payload,
nodeData = createDeviceNode(device);
note('addDevice', device.id);
network.nodes.push(nodeData);
network.lookup[nodeData.id] = nodeData;
updateNodes();
......@@ -375,10 +391,7 @@
function addLink(data) {
var link = data.payload,
lnk = createLink(link);
if (lnk) {
note('addLink', link.id);
network.links.push(lnk);
network.lookup[lnk.id] = lnk;
updateLinks();
......@@ -390,8 +403,6 @@
var host = data.payload,
node = createHostNode(host),
lnk;
note('addHost', node.id);
network.nodes.push(node);
network.lookup[host.id] = node;
updateNodes();
......@@ -406,13 +417,28 @@
network.force.start();
}
function updateDevice(data) {
var device = data.payload,
id = device.id,
nodeData = network.lookup[id];
if (nodeData) {
$.extend(nodeData, device);
updateDeviceState(nodeData);
} else {
logicError('updateDevice lookup fail. ID = "' + id + '"');
}
}
function updateHost(data) {
var host = data.payload,
hostData = network.lookup[host.id];
note('updateHost', host.id);
id = host.id,
hostData = network.lookup[id];
if (hostData) {
$.extend(hostData, host);
updateNodes();
updateHostState(hostData);
} else {
logicError('updateHost lookup fail. ID = "' + id + '"');
}
}
function showPath(data) {
......@@ -466,8 +492,7 @@
lnk;
if (!dstNode) {
// TODO: send warning message back to server on websocket
network.view.alert('switch not on map for link\n\n' +
logicError('switch not on map for link\n\n' +
'src = ' + src + '\ndst = ' + dst);
return null;
}
......@@ -500,8 +525,7 @@
dstNode = network.lookup[dst];
if (!(srcNode && dstNode)) {
// TODO: send warning message back to server on websocket
network.view.alert('nodes not on map for link\n\n' +
logicError('nodes not on map for link\n\n' +
'src = ' + src + '\ndst = ' + dst);
return null;
}
......@@ -578,11 +602,12 @@
function createDeviceNode(device) {
// start with the object as is
var node = device,
type = device.type;
type = device.type,
svgCls = type ? 'node device ' + type : 'node device';
// Augment as needed...
node.class = 'device';
node.svgClass = type ? 'node device ' + type : 'node device';
node.svgClass = device.online ? svgCls + ' online' : svgCls;
positionNode(node);
// cache label array length
......@@ -669,15 +694,24 @@
return (label && label.trim()) ? label : '.';
}
function updateDeviceState(nodeData) {
nodeData.el.classed('online', nodeData.online);
updateDeviceLabel(nodeData);
// TODO: review what else might need to be updated
}
function updateHostState(hostData) {
updateHostLabel(hostData);
// TODO: review what else might need to be updated
}
function updateNodes() {
node = nodeG.selectAll('.node')
.data(network.nodes, function (d) { return d.id; });
// operate on existing nodes, if necessary
// update host labels
node.filter('.host').select('text')
.text(hostLabel);
//node .foo() .bar() ...
// operate on entering nodes:
......@@ -828,7 +862,7 @@
webSock.ws.onmessage = function(m) {
if (m.data) {
console.log(m.data);
wsTraceRx(m.data);
handleServerEvent(JSON.parse(m.data));
}
};
......@@ -861,8 +895,25 @@
event: evType,
sid: ++sid,
payload: payload
};
webSock.send(JSON.stringify(toSend));
},
asText = JSON.stringify(toSend);
wsTraceTx(asText);
webSock.send(asText);
}
function wsTraceTx(msg) {
wsTrace('tx', msg);
}
function wsTraceRx(msg) {
wsTrace('rx', msg);
}
function wsTrace(rxtx, msg) {
console.log('[' + rxtx + '] ' + msg);
// TODO: integrate with trace view
//if (trace) {
// trace.output(rxtx, msg);
//}
}
......@@ -944,12 +995,19 @@
sc.evNumber = 0;
d3.json(urlSc, function(err, data) {
var p = data && data.params || {};
var p = data && data.params || {},
desc = data && data.description || null,
intro;
if (err) {
view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
} else {
sc.params = p;
view.alert("Scenario loaded: " + ctx + '\n\n' + data.title);
intro = "Scenario loaded: " + ctx + '\n\n' + data.title;
if (desc) {
intro += '\n\n ' + desc.join('\n ');
}
view.alert(intro);
}
});
......@@ -967,6 +1025,9 @@
fpad = fcfg.pad,
forceDim = [w - 2*fpad, h - 2*fpad];
// TODO: set trace api
//trace = onos.exported.webSockTrace;
// NOTE: view.$div is a D3 selection of the view's div
svg = view.$div.append('svg');
setSize(svg, view);
......
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Web Socket Trace -- CSS file
@author Simon Hunt
*/
#webSockTrace .toolbar {
height: 36px;
padding: 4px;
vertical-align: baseline;
font-size: 12pt;
margin-top: 6px;
}
/* theme-related */
#webSockTrace .toolbar {
background-color: #448;
color: #fff;
}
#webSockTrace .output {
overflow-y: scroll;
}
/* theme-related */
#webSockTrace .output {
background-color: #eef;
color: #226;
}
#webSockTrace .output p {
margin: 2px 8px;
font-size: 10pt;
padding-left: 6px;
}
/* theme-related */
#webSockTrace .output p.tx {
color: magenta;
}
#webSockTrace .output p.rx {
color: blue;
}
#webSockTrace .output p.subtitle {
margin: 6px 8px;
padding-left: 2px;
font-size: 12pt;
font-weight: bold;
font-style: italic;
}
/* theme-related */
#webSockTrace .output p.subtitle {
color: #626;
}
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
View that traces messages across the websocket.
@author Simon Hunt
*/
(function (onos) {
'use strict';
var v,
$d,
tb,
out,
which = 'tx',
keyDispatch = {
space: function () {
output(which, "Simon woz 'ere... " + which);
which = (which === 'tx') ? 'rx' : 'tx';
}
};
function addHeader() {
tb = $d.append('div')
.attr('class', 'toolbar');
tb.append('span').text('Web Socket Trace');
}
function addOutput() {
out = $d.append('div')
.attr('class', 'output');
}
function subtitle(msg) {
out.append('p').attr('class', 'subtitle').text(msg);
}
function output(rxtx, msg) {
out.append('p').attr('class', rxtx).text(msg);
}
// invoked only the first time the view is loaded
function preload(view, ctx, flags) {
// NOTE: view.$div is a D3 selection of the view's div
v = view;
$d = v.$div;
addHeader();
addOutput();
// hack for now, to allow topo access to our API
// TODO: add 'exportApi' and 'importApi' to views.
onos.exported.webSockTrace = {
subtitle: subtitle,
output: output
};
}
// invoked just prior to loading the view
function reset(view, ctx, flags) {
}
// invoked when the view is loaded
function load(view, ctx, flags) {
resize(view, ctx, flags);
view.setKeys(keyDispatch);
subtitle('Waiting for messages...');
}
// invoked when the view is resized
function resize(view, ctx, flags) {
var h = view.height();
out.style('height', h + 'px');
}
// == register the view here, with links to lifecycle callbacks
onos.ui.addView('webSockTrace', {
preload: preload,
load: load,
resize: resize
});
}(ONOS));