Add GUI app
Signed-off-by: YAMADA Hideki <yamada.hideki@po.ntts.co.jp> Signed-off-by: FUJITA Tomonori <fujita.tomonori@lab.ntt.co.jp>
This commit is contained in:
parent
6650a97d8d
commit
dabcfaa856
0
ryu/app/gui_topology/__init__.py
Normal file
0
ryu/app/gui_topology/__init__.py
Normal file
68
ryu/app/gui_topology/gui_topology.py
Normal file
68
ryu/app/gui_topology/gui_topology.py
Normal file
@ -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
ryu/app/gui_topology/html/index.html
Normal file
12
ryu/app/gui_topology/html/index.html
Normal file
@ -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
ryu/app/gui_topology/html/router.svg
Executable file
25
ryu/app/gui_topology/html/router.svg
Executable file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="38pt" height="26pt" viewBox="0 0 38 26" version="1.1">
|
||||
<defs>
|
||||
<clipPath id="clip1">
|
||||
<path d="M 0.0585938 0.820312 L 37 0.820312 L 37 25.820312 L 0.0585938 25.820312 L 0.0585938 0.820312 Z M 0.0585938 0.820312 "/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2">
|
||||
<path d="M 0.0585938 0.820312 L 37 0.820312 L 37 25.820312 L 0.0585938 25.820312 L 0.0585938 0.820312 Z M 0.0585938 0.820312 "/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="surface0">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0.784314%,42.352941%,60.784314%);fill-opacity:1;" d="M 36.984375 8.171875 C 36.984375 12.121094 28.75 15.324219 18.59375 15.324219 C 8.433594 15.324219 0.199219 12.121094 0.199219 8.171875 L 0.199219 18.648438 C 0.199219 22.597656 8.433594 25.800781 18.59375 25.800781 C 28.75 25.800781 36.984375 22.597656 36.984375 18.648438 L 36.984375 8.171875 "/>
|
||||
<g clip-path="url(#clip1)" clip-rule="nonzero">
|
||||
<path style="fill:none;stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:4;" d="M 36.984375 17.828125 C 36.984375 13.878906 28.75 10.675781 18.59375 10.675781 C 8.433594 10.675781 0.199219 13.878906 0.199219 17.828125 L 0.199219 7.351562 C 0.199219 3.402344 8.433594 0.199219 18.59375 0.199219 C 28.75 0.199219 36.984375 3.402344 36.984375 7.351562 L 36.984375 17.828125 Z M 36.984375 17.828125 " transform="matrix(1,0,0,-1,0,26)"/>
|
||||
</g>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0.784314%,42.352941%,60.784314%);fill-opacity:1;" d="M 18.59375 15.324219 C 28.75 15.324219 36.984375 12.121094 36.984375 8.171875 C 36.984375 4.222656 28.75 1.019531 18.59375 1.019531 C 8.433594 1.019531 0.199219 4.222656 0.199219 8.171875 C 0.199219 12.121094 8.433594 15.324219 18.59375 15.324219 "/>
|
||||
<g clip-path="url(#clip2)" clip-rule="nonzero">
|
||||
<path style="fill:none;stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:4;" d="M 18.59375 10.675781 C 28.75 10.675781 36.984375 13.878906 36.984375 17.828125 C 36.984375 21.777344 28.75 24.980469 18.59375 24.980469 C 8.433594 24.980469 0.199219 21.777344 0.199219 17.828125 C 0.199219 13.878906 8.433594 10.675781 18.59375 10.675781 Z M 18.59375 10.675781 " transform="matrix(1,0,0,-1,0,26)"/>
|
||||
</g>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 14.394531 5.375 L 15.910156 7.652344 L 10.167969 8.980469 L 11.425781 7.9375 L 2.550781 6.417969 L 4.773438 4.75 L 13.339844 6.199219 L 14.394531 5.375 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 22.472656 10.898438 L 21.4375 8.550781 L 26.617188 7.515625 L 25.71875 8.320312 L 34.351562 9.796875 L 32.277344 11.453125 L 23.699219 9.839844 L 22.472656 10.898438 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 19.640625 4.132812 L 25.441406 2.542969 L 25.511719 5.027344 L 24.058594 4.753906 L 21.230469 7.101562 L 18.527344 6.707031 L 21.449219 4.410156 L 19.640625 4.132812 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 17.152344 13.039062 L 11.628906 14.074219 L 11.421875 11.519531 L 13.011719 11.867188 L 16.050781 9.269531 L 18.742188 9.726562 L 15.496094 12.558594 L 17.152344 13.039062 "/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
30
ryu/app/gui_topology/html/ryu.topology.css
Normal file
30
ryu/app/gui_topology/html/ryu.topology.css
Normal file
@ -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
ryu/app/gui_topology/html/ryu.topology.js
Normal file
250
ryu/app/gui_topology/html/ryu.topology.js
Normal file
@ -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()
|
Loading…
Reference in New Issue
Block a user