Skip to content

Commit

Permalink
Add GUI app
Browse files Browse the repository at this point in the history
Signed-off-by: YAMADA Hideki <yamada.hideki@po.ntts.co.jp>
Signed-off-by: FUJITA Tomonori <fujita.tomonori@lab.ntt.co.jp>
  • Loading branch information
YAMADA Hideki authored and fujita committed Jun 17, 2014
1 parent 6650a97 commit dabcfaa
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 0 deletions.
Empty file.
68 changes: 68 additions & 0 deletions ryu/app/gui_topology/gui_topology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copyright (C) 2014 Nippon Telegraph and Telephone Corporation.
#
# 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.

"""
Usage example
1. Join switches (use your favorite method):
$ sudo mn --controller remote --topo tree,depth=3
2. Run this application:
$ PYTHONPATH=. ./bin/ryu run \
--observe-links ryu/app/gui_topology/gui_topology.py
3. Access http://<ip address of ryu host>:8080 with your web browser.
"""

import os

from webob.static import DirectoryApp

from ryu.app.wsgi import ControllerBase, WSGIApplication, route
from ryu.base import app_manager


PATH = os.path.dirname(__file__)


# Serving static files
class GUIServerApp(app_manager.RyuApp):
_CONTEXTS = {
'wsgi': WSGIApplication,
}

def __init__(self, *args, **kwargs):
super(GUIServerApp, self).__init__(*args, **kwargs)

wsgi = kwargs['wsgi']
wsgi.register(GUIServerController)


class GUIServerController(ControllerBase):
def __init__(self, req, link, data, **config):
super(GUIServerController, self).__init__(req, link, data, **config)
path = "%s/html/" % PATH
self.static_app = DirectoryApp(path)

@route('topology', '/{filename:.*}')
def static_handler(self, req, **kwargs):
if kwargs['filename']:
req.path_info = kwargs['filename']
return self.static_app(req)


app_manager.require_app('ryu.app.rest_topology')
app_manager.require_app('ryu.app.ws_topology')
app_manager.require_app('ryu.app.ofctl_rest')
12 changes: 12 additions & 0 deletions ryu/app/gui_topology/html/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="./ryu.topology.css">
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<h1>Ryu Topology Viewer</h1>
<script src="./ryu.topology.js" charset="utf-8"></script>
</body>
</html>
25 changes: 25 additions & 0 deletions ryu/app/gui_topology/html/router.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions ryu/app/gui_topology/html/ryu.topology.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#topology {
border: 1px solid #000000;
}

.node {
}

.node.fixed {
fill: #C0C0C0;
}

.node text {
font-size: 14px;
}

.link {
stroke: #090909;
stroke-opacity: .6;
stroke-width: 2px;
}

.port circle {
stroke: black;
fill: #C5F9F9;
}

.port text {
font-size: 10px;
}

250 changes: 250 additions & 0 deletions ryu/app/gui_topology/html/ryu.topology.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
var CONF = {
image: {
width: 50,
height: 40
},
force: {
width: 960,
height: 500,
dist: 200,
charge: -600
}
};

var ws = new WebSocket("ws://" + location.host + "/v1.0/topology/ws");
ws.onmessage = function(event) {
var data = JSON.parse(event.data);

var result = rpc[data.method](data.params);

var ret = {"id": data.id, "jsonrpc": "2.0", "result": result};
this.send(JSON.stringify(ret));
}

function trim_zero(s) {
return s.replace(/^0+/, "");
}

function dpid_to_int(dpid) {
return Number("0x" + dpid);
}

var elem = {
force: d3.layout.force()
.size([CONF.force.width, CONF.force.height])
.charge(CONF.force.charge)
.linkDistance(CONF.force.dist)
.on("tick", _tick),
svg: d3.select("body").append("svg")
.attr("id", "topology")
.attr("width", CONF.force.width)
.attr("height", CONF.force.height),
console: d3.select("body").append("div")
.attr("id", "console")
.attr("width", CONF.force.width)
};
function _tick() {
elem.link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });

elem.node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

elem.port.attr("transform", function(d) {
var p = topo.get_port_point(d);
return "translate(" + p.x + "," + p.y + ")";
});
}
elem.drag = elem.force.drag().on("dragstart", _dragstart);
function _dragstart(d) {
var dpid = dpid_to_int(d.dpid)
d3.json("/stats/flow/" + dpid, function(e, data) {
flows = data[dpid];
console.log(flows);
elem.console.selectAll("ul").remove();
li = elem.console.append("ul")
.selectAll("li");
li.data(flows).enter().append("li")
.text(function (d) { return JSON.stringify(d, null, " "); });
});
d3.select(this).classed("fixed", d.fixed = true);
}
elem.node = elem.svg.selectAll(".node");
elem.link = elem.svg.selectAll(".link");
elem.port = elem.svg.selectAll(".port");
elem.update = function () {
this.force
.nodes(topo.nodes)
.links(topo.links)
.start();

this.link = this.link.data(topo.links);
this.link.exit().remove();
this.link.enter().append("line")
.attr("class", "link");

this.node = this.node.data(topo.nodes);
// NOTE: Removing node is not supported.
var nodeEnter = this.node.enter().append("g")
.attr("class", "node")
.on("dblclick", function(d) { d3.select(this).classed("fixed", d.fixed = false); })
.call(this.drag);
nodeEnter.append("image")
.attr("xlink:href", "./router.svg")
.attr("x", -CONF.image.width/2)
.attr("y", -CONF.image.height/2)
.attr("width", CONF.image.width)
.attr("height", CONF.image.height);
nodeEnter.append("text")
.attr("dx", -CONF.image.width/2)
.attr("dy", CONF.image.height-10)
.text(function(d) { return "dpid: " + trim_zero(d.dpid); });

var ports = topo.get_ports();
this.port.remove();
this.port = this.svg.selectAll(".port").data(ports);
var portEnter = this.port.enter().append("g")
.attr("class", "port");
portEnter.append("circle")
.attr("r", 8);
portEnter.append("text")
.attr("dx", -3)
.attr("dy", 3)
.text(function(d) { return trim_zero(d.port_no); });
};

function is_valid_link(link) {
return (link.src.dpid < link.dst.dpid)
}

var topo = {
nodes: [],
links: [],
node_index: {}, // dpid -> index of nodes array
initialize: function (data) {
this.add_nodes(data.switches);
this.add_links(data.links);
},
add_nodes: function (switches) {
for (var i = 0; i < switches.length; i++) {
this.nodes[i] = switches[i];
}

this.node_index = {};
for (var i = 0; i < this.nodes.length; i++) {
this.node_index[this.nodes[i].dpid] = i;
}
},
add_links: function (links) {
for (var i = 0; i < links.length; i++) {
if (!is_valid_link(links[i])) continue;
console.log("add link: " + JSON.stringify(links[i]));

var src_dpid = links[i].src.dpid;
var dst_dpid = links[i].dst.dpid;
var src_index = this.node_index[src_dpid];
var dst_index = this.node_index[dst_dpid];
var link = {
source: src_index,
target: dst_index,
port: {
src: links[i].src,
dst: links[i].dst
}
}
this.links.push(link);
}
},
delete_links: function (links) {
for (var i = 0; i < links.length; i++) {
if (!is_valid_link(links[i])) continue;
console.log("delete link: " + JSON.stringify(links[i]));

link_index = this.get_link_index(links[i]);
this.links.splice(link_index, 1);
}
},
get_link_index: function (link) {
for (var i = 0; i < this.links.length; i++) {
if (link.src.dpid == this.links[i].port.src.dpid &&
link.src.port_no == this.links[i].port.src.port_no &&
link.dst.dpid == this.links[i].port.dst.dpid &&
link.dst.port_no == this.links[i].port.dst.port_no) {
return i;
}
}
return null;
},
get_ports: function () {
var ports = [];
var pushed = {};
for (var i = 0; i < this.links.length; i++) {
function _push(p, dir) {
key = p.dpid + ":" + p.port_no
if (key in pushed) {
return 0;
}

pushed[key] = true;
p.link_idx = i;
p.link_dir = dir;
return ports.push(p);
}
_push(this.links[i].port.src, "source");
_push(this.links[i].port.dst, "target");
}

return ports;
},
get_port_point: function (d) {
var weight = 0.88;

var link = this.links[d.link_idx];
var x1 = link.source.x;
var y1 = link.source.y;
var x2 = link.target.x;
var y2 = link.target.y;

if (d.link_dir == "target") weight = 1.0 - weight;

var x = x1 * weight + x2 * (1.0 - weight);
var y = y1 * weight + y2 * (1.0 - weight);

return {x: x, y: y};
},
}

var rpc = {
event_switch_enter: function (params) {
console.log("Not Implemented: event_switch_enter, " + JSON.stringify(params));
},
event_switch_leave: function (params) {
console.log("Not Implemented: event_switch_leave, " + JSON.stringify(params));
},
event_link_add: function (links) {
topo.add_links(links);
elem.update();
return "";
},
event_link_delete: function (links) {
topo.delete_links(links);
elem.update();
return "";
},
}

function initialize_topology() {
d3.json("/v1.0/topology/switches", function(error, switches) {
d3.json("/v1.0/topology/links", function(error, links) {
topo.initialize({switches: switches, links: links});
elem.update();
});
});
}

function main() {
initialize_topology();
}

main()

0 comments on commit dabcfaa

Please sign in to comment.