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()