Added simple topology gui
Change-Id: I313e8460c3b2c9c58ac815f64a24e9bc2587f0b6
diff --git a/web/js/ctrl.js b/web/js/ctrl.js
new file mode 100644
index 0000000..9a093e3
--- /dev/null
+++ b/web/js/ctrl.js
@@ -0,0 +1,160 @@
+function data_convert(registry_raw){
+ var controller_map={};
+ var controllers=[];
+ for (var r in registry_raw){
+ var controller={};
+ if ( !(registry_raw[r][0]['controllerId'] in controller_map) ){
+ controller.name=registry_raw[r][0]['controllerId'];
+ controller.onos=1;
+ controllers.push(controller);
+ controller_map[registry_raw[r][0]['controllerId']] = 1;
+ }
+ }
+// controllers.sort(
+// function(a,b){
+// if(a.name < b.name)
+// return -1;
+// if(a.name > b.name)
+// return 1;
+// return 0;
+// }
+// );
+ return controllers;
+}
+
+function controller_status(registry_url){
+/* var data = [{name:'onos9vpc', onos: 1, cassandra: 1},
+ {name:'onos10vpc', onos: 0, cassandra: 1},
+ {name:'onos11vpc', onos: 1, cassandra: 1},
+ {name:'onos12vpc', onos: 0, cassandra: 1}] */
+
+ var barWidth = 100;
+ var width = (barWidth + 10) * 8
+ var height = 50;
+
+ var ContStatus = d3.select("#onos-status").
+ append("svg:svg").
+ attr("width", 1280).
+ attr("height", 50);
+
+ setInterval(function() {
+ $.ajax({
+ url: registry_url,
+ success: function(registry) {
+ draw(data_convert(registry));
+ },
+ error: function() {
+ return;
+ },
+ dataType: "json"
+ });
+ }, 3000);
+ function draw(json){
+ var data = json;
+ var controller_rect = ContStatus.selectAll("rect").data(data);
+ var controller_text = ContStatus.selectAll("text").data(data);
+
+ var x = d3.scale.linear().domain([0, data.length]).range([0, width]);
+ var y = d3.scale.linear().domain([0, d3.max(data, function(datum) { return datum.onos; })]).rangeRound([0, height]);
+
+ console.log(data)
+ controller_rect.
+ enter().
+ append("svg:rect").
+ attr("x", function(datum, index) { return x(index); }).
+ attr("y", function(datum) { return height - y(datum.onos); }).
+ attr("height", function(datum) { return y(datum.onos); }).
+ attr("width", barWidth).
+ attr("fill", function(datum, index) {
+ if (index == 0){
+ return "red"
+ }else if (index == 1){
+ return "blue"
+ }else if (index == 2){
+ return "green"
+ }else if (index == 3){
+ return "orange"
+ }else if (index == 4){
+ return "cyan"
+ }else if (index == 5){
+ return "magenta"
+ }else if (index == 6){
+ return "yellow"
+ }else if (index == 7){
+ return "purple"
+ }else{
+ return "black"
+ }
+ });
+
+ controller_text.
+ enter().
+ append("svg:text").
+ text(function(datum) { return datum.name; }).
+ attr("x", function(datum, index) { return x(index)+10; }).
+ attr("y", function(datum) { return 30 ; }).
+ attr("height", function(datum) { return y(datum.onos); }).
+ attr("width", barWidth).
+ attr('fill', 'white');
+
+
+ controller_rect.
+ attr("x", function(datum, index) { return x(index); }).
+ attr("y", function(datum) { return height - y(datum.onos); }).
+ attr("height", function(datum) { return y(datum.onos); }).
+ attr("width", barWidth).
+ attr("fill", function(datum, index) {
+ if (index == 0){
+ return "red"
+ }else if (index == 1){
+ return "blue"
+ }else if (index == 2){
+ return "green"
+ }else if (index == 3){
+ return "orange"
+ }else if (index == 4){
+ return "cyan"
+ }else if (index == 5){
+ return "magenta"
+ }else if (index == 6){
+ return "yellow"
+ }else if (index == 7){
+ return "purple"
+ }else{
+ return "black"
+ }
+ });
+
+ controller_text.
+ text(function(datum) { return datum.name; }).
+ attr("x", function(datum, index) { return x(index)+10; }).
+ attr("y", function(datum) { return 30 ; }).
+ attr("height", function(datum) { return y(datum.onos); }).
+ attr("width", barWidth).
+ attr('fill', 'white');
+
+ controller_rect.exit().remove();
+ controller_text.exit().remove();
+ }
+/*
+ $("#more").click( function() {
+ $.ajax({
+ url: 'http://gui.onlab.us:8080/controller_status1',
+ success: function(json) {
+ draw(json);
+ },
+ dataType: "json"
+ });
+ });
+ $("#less").click( function() {
+ $.ajax({
+ url: 'http://gui.onlab.us:8080/controller_status2',
+ success: function(json) {
+ draw(json);
+ },
+ dataType: "json"
+ });
+ });
+*/
+}
+
diff --git a/web/js/topo.js b/web/js/topo.js
new file mode 100644
index 0000000..b518bd2
--- /dev/null
+++ b/web/js/topo.js
@@ -0,0 +1,435 @@
+function convert_to_topodata(switches, links, registry){
+ var controllers={};
+ var nr_controllers=0;
+ var sws=[];
+ var ls=[];
+ var topo = new Array();
+ switches.forEach(function(item) {
+ var sw={}
+ sw.name=item.dpid;
+ sw.group=-1;
+ sws.push(sw);
+ });
+ for (var r in registry){
+ if ( ! (registry[r][0]['controllerId'] in controllers) ){
+ controllers[registry[r][0]['controllerId']] = ++nr_controllers;
+ }
+ }
+ for (var i = 0; i < sws.length; i++){
+ if (sws[i].name in registry){
+ sws[i].group=controllers[registry[sws[i].name][0]['controllerId']];
+ sws[i].controller=registry[sws[i].name][0]['controllerId'];
+ }
+ }
+ links.forEach(function(item) {
+ var link={};
+ for (var i = 0; i < sws.length; i++){
+ if(sws[i].name == item['src-switch'])
+ break;
+ }
+ link.source=i;
+ for (var i = 0; i < sws.length; i++){
+ if(sws[i].name == item['dst-switch'])
+ break;
+ }
+ link.target=i;
+ ls.push(link);
+ });
+ topo['nodes']=sws;
+ topo['links']=ls;
+ return topo;
+}
+
+var width = 1280;
+var height = 1280;
+var radius = 8;
+function gui(switch_url, link_url, registry_url){
+ var svg = d3.select("#topology")
+ .append("svg:svg")
+ .attr("width", width)
+ .attr("height", height);
+
+ var force = d3.layout.force()
+ .charge(-500)
+ .linkDistance(100)
+ .size([width, height]);
+
+ var node_drag = d3.behavior.drag()
+ .on("dragstart", dragstart)
+ .on("drag", dragmove)
+ .on("dragend", dragend);
+
+ var color = d3.scale.category20();
+ var topodata;
+ var nodes = force.nodes();
+ var links = force.links();
+
+ d3.json(switch_url, function(error, rest_switches) {
+ d3.json(link_url, function(error, rest_links) {
+ d3.json(registry_url, function(error, rest_registry) {
+ topodata = convert_to_topodata(rest_switches, rest_links, rest_registry);
+ init(topodata, nodes, links);
+ path = svg.append("svg:g").selectAll("path").data(links);
+ circle = svg.append("svg:g").selectAll("circle").data(nodes);
+ text = svg.append("svg:g").selectAll("text").data(nodes);
+ draw();
+ });
+ });
+ });
+
+ setInterval(function(){
+ d3.json(switch_url, function(error, rest_switches) {
+ d3.json(link_url, function(error, rest_links) {
+ d3.json(registry_url, function(error, rest_registry) {
+ topodata = convert_to_topodata(rest_switches, rest_links, rest_registry);
+ var changed = update(topodata, nodes, links);
+ path = svg.selectAll("path").data(links)
+ circle = svg.selectAll("circle").data(nodes);
+ text = svg.selectAll("text").data(nodes);
+ if ( changed ){
+ draw();
+ }
+ });
+ });
+ });
+ }, 3000);
+
+ function draw(){
+ force.stop();
+ svg.append("svg:text")
+ .attr("x", 50)
+ .attr("y", 20)
+ .text(function(){return "Switch: " + force.nodes().length + " (Active: " + nr_active_sw() + ")/ Link: " + force.links().length});
+
+ path.enter().append("svg:path")
+ .attr("class", function(d) { return "link"; })
+ .attr("marker-end", function(d) {
+ if(d.type == 1){
+ return "url(#TriangleRed)";
+ } else {
+ return "url(#Triangle)";
+ }
+ });
+
+ circle.enter().append("svg:circle")
+ .attr("r", function(d) {
+ if (d.group == 1000){
+ return radius;
+ }else{
+ return radius;
+ }
+ })
+ .call(node_drag);
+
+ text.enter().append("svg:text")
+ .attr("x", radius)
+ .attr("y", ".31em")
+ .text(function(d) {
+ l=d.name.split(":").length
+ return d.name.split(":")[l-2] + ":" + d.name.split(":")[l-1]
+ });
+
+ circle.append("title")
+ .text(function(d) { return d.name; });
+
+ circle.attr("fill", function(d) {
+ if (d.group == 1){
+ return "red"
+ }else if (d.group == 2){
+ return "blue"
+ }else if (d.group == 3){
+ return "green"
+ }else if (d.group == 4){
+ return "orange"
+ }else if (d.group == 5){
+ return "cyan"
+ }else if (d.group == 6){
+ return "magenta"
+ }else if (d.group == 7){
+ return "yellow"
+ }else if (d.group == 8){
+ return "purple"
+ }else{
+ return "gray"
+ }
+ });
+
+ circle.on('mouseover', function(d){
+ var nodeSelection = d3.select(this).style({opacity:'0.8'});
+ nodeSelection.select("text").style({opacity:'1.0'});
+ });
+
+
+ path.attr("stroke", function(d) {
+ if(d.type == 1){
+ return "red"
+ } else {
+ return "black"
+ }
+ }).attr("stroke-width", function(d) {
+ if(d.type == 1){
+ return "2px";
+ } else {
+ return "1.5px";
+ }
+ }).attr("marker-end", function(d) {
+ if(d.type == 1){
+ return "url(#TriangleRed)";
+ } else {
+ return "url(#Triangle)";
+ }
+ });
+ path.exit().remove();
+ circle.exit().remove();
+ text.exit().remove();
+ force.on("tick", tick);
+ force.start();
+ }
+ function nr_active_sw(){
+ var n=0;
+ var nodes = force.nodes();
+ for(var i=0;i<nodes.length;i++){
+ if(nodes[i].group!=0)
+ n++;
+ };
+ return n;
+ }
+ function dragstart(d, i) {
+ force.stop() // stops the force auto positioning before you start dragging
+ }
+ function dragmove(d, i) {
+ d.px += d3.event.dx;
+ d.py += d3.event.dy;
+ d.x += d3.event.dx;
+ d.y += d3.event.dy;
+ tick(); // this is the key to make it work together with updating both px,py,x,y on d !
+ }
+
+ function dragend(d, i) {
+ d.fixed = true; // of course set the node to fixed so the force doesn't include the node in its auto positioning stuff
+ tick();
+ force.resume();
+ }
+ function tick() {
+ path.attr("d", function(d) {
+ var dx = d.target.x - d.source.x,
+ dy = d.target.y - d.source.y,
+ dr = 1/d.linknum; //linknum is defined above
+ dr = 0; // 0 for direct line
+ return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
+ });
+ path.attr("stroke", function(d) {
+ if(d.type == 1){
+ return "red"
+ } else {
+ return "black"
+ }
+ }).attr("stroke-width", function(d) {
+ if(d.type == 1){
+ return "3px";
+ } else {
+ return "1.5px";
+ }
+ }).attr("marker-end", function(d) {
+ if(d.type == 1){
+ return "url(#TriangleRed)";
+ } else {
+ return "url(#Triangle)";
+ }
+ });
+// circle.attr("cx", function(d) { return d.x; }).attr("cy", function(d) { return d.y; });
+ circle.attr("transform", function(d) {
+ x = Math.max(radius, Math.min(width - radius, d.x));
+ y = Math.max(radius, Math.min(height - radius, d.y));
+// return "translate(" + d.x + "," + d.y + ")";
+ return "translate(" + x + "," + y + ")";
+ })
+ circle.attr("fill", function(d) {
+ if (d.group == 1){
+ return "red"
+ }else if (d.group == 2){
+ return "blue"
+ }else if (d.group == 3){
+ return "green"
+ }else if (d.group == 4){
+ return "orange"
+ }else if (d.group == 5){
+ return "cyan"
+ }else if (d.group == 6){
+ return "magenta"
+ }else if (d.group == 7){
+ return "yellow"
+ }else if (d.group == 8){
+ return "purple"
+ }else{
+ return "gray"
+ }
+ });
+// text.attr("x", function(d) { return d.x; }).attr("y", function(d) { return d.y; });
+ text.attr("transform", function(d) {
+ return "translate(" + d.x + "," + d.y + ")";
+ });
+ }
+
+}
+
+
+function init(topodata, nodes, links){
+ topodata.nodes.forEach(function(item) {
+ nodes.push(item);
+ });
+ topodata.links.forEach(function(item) {
+ links.push(item);
+ });
+ links.sort(compare_link);
+ // distinguish links that have the same src & dst node by 'linknum'
+ for (var i=1; i < links.length; i++) {
+ if (links[i].source == links[i-1].source &&
+ links[i].target == links[i-1].target) {
+ links[i].linknum = links[i-1].linknum + 1;
+ } else {
+ links[i].linknum = 1;
+ };
+ };
+}
+
+function compare_link (a, b){
+ if (a.source > b.source)
+ return 1;
+ else if (a.source < b.source)
+ return -1;
+ else {
+ if (a.target > b.target)
+ return 1;
+ else if (a.target < b.target)
+ return -1;
+ else
+ return 0;
+ }
+}
+
+/* Return nodes that is not in the current list of nodes */
+Array.prototype.node_diff = function(arr) {
+ return this.filter(function(i) {
+ for (var j = 0; j < arr.length ; j++) {
+ if (arr[j].name === i.name)
+ return false;
+ }
+ return true;
+ });
+};
+
+/* Return removed links */
+function gone_links (topo_json, links){
+ gone = []
+ for (var i = 0; i < links.length ; i ++){
+ var found = 0;
+ for (var j = 0; j < topo_json.links.length ; j ++){
+ if (links[i].source.name == topo_json.nodes[topo_json.links[j].source].name &&
+ links[i].target.name == topo_json.nodes[topo_json.links[j].target].name ){
+ found = 1;
+ break;
+ }
+ }
+ if ( found == 0 ){
+ gone.push(links[i]);
+ }
+ }
+ return gone;
+}
+
+/* Return added links */
+function added_links (topo_json, links) {
+ added = [];
+ for (var j = 0; j < topo_json.links.length ; j ++){
+ var found = 0;
+ for (var i = 0; i < links.length ; i ++){
+ if (links[i].source.name == topo_json.nodes[topo_json.links[j].source].name &&
+ links[i].target.name == topo_json.nodes[topo_json.links[j].target].name ){
+ found = 1;
+ break;
+ }
+ }
+ if ( found == 0 ){
+ added.push(topo_json.links[j]);
+ }
+ }
+ return added;
+}
+
+/* check if toplogy has changed and update node[] and link[] accordingly */
+function update(json, nodes, links){
+ var changed = false;
+ var n_adds = json.nodes.node_diff(nodes);
+ var n_rems = nodes.node_diff(json.nodes);
+ for (var i = 0; i < n_adds.length; i++) {
+ nodes.push(n_adds[i]);
+ changed = true;
+ }
+ for (var i = 0; i < n_rems.length; i++) {
+ for (var j = 0; j < nodes.length; j++) {
+ if ( nodes[j].name == n_rems[i].name ){
+ nodes.splice(j,1);
+ changed = true;
+ break;
+ }
+ }
+ }
+ var l_adds = added_links(json, links);
+ var l_rems = gone_links(json, links);
+ for (var i = 0; i < l_rems.length ; i++) {
+ for (var j = 0; j < links.length; j++) {
+ if (links[j].source.name == l_rems[i].source.name &&
+ links[j].target.name == l_rems[i].target.name) {
+ links.splice(j,1);
+ changed = true;
+ break;
+ }
+ }
+ }
+ // Sorce/target of an element of l_adds[] are corresponding to the index of json.node[]
+ // which is different from the index of node[] (new nodes are always added to the last)
+ // So update soure/target node indexes of l_add[] need to be fixed to point to the proper
+ // node in node[];
+ for (var i = 0; i < l_adds.length; i++) {
+ for (var j = 0; j < nodes.length; j++) {
+ if ( json.nodes[l_adds[i].source].name == nodes[j].name ){
+ l_adds[i].source = j;
+ break;
+ }
+ }
+ for (var j = 0; j < nodes.length; j++) {
+ if ( json.nodes[l_adds[i].target].name == nodes[j].name ){
+ l_adds[i].target = j;
+ break;
+ }
+ }
+ links.push(l_adds[i]);
+ changed = true;
+ }
+
+ // Update "group" attribute of nodes
+ for (var i = 0; i < nodes.length; i++) {
+ for (var j = 0; j < json.nodes.length; j++) {
+ if ( nodes[i].name == json.nodes[j].name ){
+ if (nodes[i].group != json.nodes[j].group){
+ nodes[i].group = json.nodes[j].group;
+ changed = true;
+ }
+ }
+ }
+ }
+ for (var i = 0; i < links.length; i++) {
+ for (var j = 0; j < json.links.length; j++) {
+ if (links[i].target.name == json.nodes[json.links[j].target].name &&
+ links[i].source.name == json.nodes[json.links[j].source].name ){
+ if (links[i].type != json.links[j].type){
+ links[i].type = json.links[j].type;
+ changed = true;
+ }
+ }
+ }
+ }
+ return changed
+}
+
diff --git a/web/simple-topo.html b/web/simple-topo.html
new file mode 100644
index 0000000..053c67c
--- /dev/null
+++ b/web/simple-topo.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<style>
+circle {
+ stroke: #333;
+ stroke-width: 1.5px;
+}
+
+text {
+ font: 20px sans-serif;
+ pointer-events: none;
+}
+
+</style>
+<head>
+<title>ONOS Simple Topology GUI</title>
+<script src="http://d3js.org/d3.v3.js"></script>
+<script src="http://d3js.org/queue.v1.js"></script>
+<script type="text/javascript" src="js/topo.js"></script>
+<script type="text/javascript" src="js/jquery-1.7.2.min.js"></script>
+</head>
+<body>
+<!--
+<button id="more">more</button>
+<button id="less">less</button>
+--!>
+<svg width="0.1in" height="0.1in"
+ viewBox="0 0 4000 2000" version="1.1"
+ xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <marker id="Triangle"
+ viewBox="0 -5 10 10" refX="15" refY="-1.5"
+ markerUnits="strokeWidth"
+ markerWidth="6" markerHeight="6"
+ orient="auto">
+ <path d="M0,-5L10,0L0,5"/>
+ </marker>
+ <marker id="TriangleRed"
+ viewBox="0 -5 10 10" refX="10" refY="-0.2"
+ markerUnits="strokeWidth"
+ markerWidth="6" markerHeight="6"
+ orient="auto">
+ <path d="M0,-5L10,0L0,5" fill="red" stroke="red"/>
+ </marker>
+ </defs>
+<h1>ONOS Simple Topology GUI</h1>
+<h2>Controller Status</h2>
+<div id="servers"></div>
+<div id="onos-status"></div>
+<h2>Topology View</h2>
+<div id="topology"></div>
+<script type="text/javascript" src="js/ctrl.js"></script>
+<script type="text/javascript">
+controller_status("wm/onos/registry/switches/json");
+gui("wm/onos/topology/switches/json","wm/onos/topology/links/json","wm/onos/registry/switches/json");
+</script>
+</svg>
+</body>
+</html>
diff --git a/web/simple_web_server.py b/web/simple_web_server.py
new file mode 100755
index 0000000..14fa05d
--- /dev/null
+++ b/web/simple_web_server.py
@@ -0,0 +1,71 @@
+#! /usr/bin/env python
+import json
+import argparse
+import time
+import re
+from urllib2 import Request, urlopen, URLError, HTTPError
+from flask import Flask, make_response, request
+
+## Global Var for ON.Lab local REST ##
+RestIP="localhost"
+RestPort=8080
+DEBUG=1
+
+app = Flask(__name__)
+
+### Serving Static Files ###
+@app.route('/', methods=['GET'])
+@app.route('/<filename>', methods=['GET'])
+@app.route('/js/<filename>', methods=['GET'])
+def return_file(filename):
+ if request.path == "/":
+ fullpath = "./simple-topo.html"
+ else:
+ fullpath = str(request.path)[1:]
+
+ try:
+ open(fullpath)
+ except:
+ response = make_response("Cannot find a file: %s" % (fullpath), 500)
+ response.headers["Content-type"] = "text/html"
+ return response
+
+ response = make_response(open(fullpath).read())
+ suffix = fullpath.split(".")[-1]
+
+ if suffix == "html" or suffix == "htm":
+ response.headers["Content-type"] = "text/html"
+ elif suffix == "js":
+ response.headers["Content-type"] = "application/javascript"
+ elif suffix == "css":
+ response.headers["Content-type"] = "text/css"
+ elif suffix == "png":
+ response.headers["Content-type"] = "image/png"
+ elif suffix == "svg":
+ response.headers["Content-type"] = "image/svg+xml"
+
+ return response
+
+## Proxying REST calls ###
+@app.route('/wm/', defaults={'path':''})
+@app.route('/wm/<path:path>')
+def rest(path):
+ url="http://localhost:8080/wm/" + path
+ print url
+ try:
+ response = urlopen(url)
+ except URLError, e:
+ print "ONOS REST IF %s has issue. Reason: %s" % (url, e.reason)
+ result = ""
+ except HTTPError, e:
+ print "ONOS REST IF %s has issue. Code %s" % (url, e.code)
+ result = ""
+
+ print response
+ result = response.read()
+ return result
+
+if __name__ == "__main__":
+ app.debug = True
+ app.run(threaded=True, host="0.0.0.0", port=9000)
+# app.run(threaded=False, host="0.0.0.0", port=9000)