[spp] [PATCH 10/13] controller: add topo.py
ogawa.yasufumi at lab.ntt.co.jp
ogawa.yasufumi at lab.ntt.co.jp
Tue Mar 6 11:50:52 CET 2018
From: Yasufumi Ogawa <ogawa.yasufumi at lab.ntt.co.jp>
This update adds 'topo.py' in which methods for 'topo' command are
implemented.
Signed-off-by: Yasufumi Ogawa <ogawa.yasufumi at lab.ntt.co.jp>
---
src/controller/topo.py | 312 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 312 insertions(+)
create mode 100644 src/controller/topo.py
diff --git a/src/controller/topo.py b/src/controller/topo.py
new file mode 100644
index 0000000..30a9c1a
--- /dev/null
+++ b/src/controller/topo.py
@@ -0,0 +1,312 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+import os
+import re
+from spp_common import logger
+import subprocess
+import traceback
+import uuid
+import websocket
+
+
+class Topo(object):
+ """Setup and output network topology for topo command
+
+ Topo command supports four types of output.
+ * terminal (but very few terminals supporting to display images)
+ * browser (websocket server is required)
+ * image file (jpg, png, bmp)
+ * text (dot, json, yaml)
+ """
+
+ def __init__(self, sec_ids, m2s_queues, s2m_queues):
+ logger.info("Topo initialized with sec IDs %s" % sec_ids)
+ self.sec_ids = sec_ids
+ self.m2s_queues = m2s_queues
+ self.s2m_queues = s2m_queues
+
+ def show(self, dtype):
+ res_ary = []
+ for sec_id in self.sec_ids:
+ self.m2s_queues[sec_id].put("status")
+ res = self.format_sec_status(self.s2m_queues[sec_id].get(True))
+ res_ary.append(res)
+ if dtype == "http":
+ self.to_http(res_ary)
+ elif dtype == "term":
+ self.to_term(res_ary)
+ else:
+ print("Invalid file type")
+ return res_ary
+
+ def output(self, fname, ftype="dot"):
+ res_ary = []
+ for sec_id in self.sec_ids:
+ self.m2s_queues[sec_id].put("status")
+ res = self.format_sec_status(self.s2m_queues[sec_id].get(True))
+ res_ary.append(res)
+
+ if ftype == "dot":
+ self.to_dot(res_ary, fname)
+ elif ftype == "json" or ftype == "js":
+ self.to_json(res_ary, fname)
+ elif ftype == "yaml" or ftype == "yml":
+ self.to_yaml(res_ary, fname)
+ elif ftype == "jpg" or ftype == "png" or ftype == "bmp":
+ self.to_img(res_ary, fname)
+ else:
+ print("Invalid file type")
+ return res_ary
+ print("Create topology: '%s'" % fname)
+ return res_ary
+
+ def to_dot(self, sec_list, output_fname):
+ # Label given if outport is "none"
+ NO_PORT = "none"
+
+ # Graphviz params
+ SEC_COLORS = [
+ "blue", "green", "orange", "chocolate", "black",
+ "cyan3", "green3", "indianred", "lawngreen", "limegreen"]
+ PORT_COLORS = {
+ "PHY": "white",
+ "RING": "yellow",
+ "VHOST": "limegreen"}
+ LINE_STYLE = {
+ "RUNNING": "solid",
+ "IDLING": "dashed"}
+ GRAPH_TYPE = "digraph"
+ LINK_TYPE = "->"
+
+ node_attrs = 'node[shape="rectangle", style="filled"];'
+
+ phys = []
+ rings = []
+ vhosts = []
+ links = []
+
+ for sec in sec_list:
+ for port in sec["ports"]:
+ if port["iface"]["type"] == "PHY":
+ phys.append(port)
+ elif port["iface"]["type"] == "RING":
+ rings.append(port)
+ elif port["iface"]["type"] == "VHOST":
+ vhosts.append(port)
+ else:
+ raise ValueError(
+ "Invaid interface type: %s" % port["iface"]["type"])
+
+ if port["out"] != NO_PORT:
+ out_id = int(port["out"])
+ if sec["forward"] is True:
+ l_style = LINE_STYLE["RUNNING"]
+ else:
+ l_style = LINE_STYLE["IDLING"]
+ attrs = '[label="%s", color="%s", style="%s"]' % (
+ "sec" + sec["sec_id"],
+ SEC_COLORS[int(sec["sec_id"])],
+ l_style
+ )
+ tmp = "%s%s %s %s%s%s;" % (
+ port["iface"]["type"],
+ port["iface"]["id"],
+ LINK_TYPE,
+ sec["ports"][out_id]["iface"]["type"],
+ sec["ports"][out_id]["iface"]["id"],
+ attrs
+ )
+ links.append(tmp)
+
+ output = ["%s spp{" % GRAPH_TYPE]
+ output.append("newrank=true;")
+ output.append(node_attrs)
+
+ phy_labels = []
+ for p in phys:
+ phy_labels.append(p["iface"]["type"] + p["iface"]["id"])
+ phy_labels = list(set(phy_labels))
+ for l in phy_labels:
+ output.append(
+ '%s[label="%s", fillcolor="%s"];' % (
+ l, l, PORT_COLORS["PHY"]))
+
+ ring_labels = []
+ for p in rings:
+ ring_labels.append(p["iface"]["type"] + p["iface"]["id"])
+ ring_labels = list(set(ring_labels))
+ for l in ring_labels:
+ output.append(
+ '%s[label="%s", fillcolor="%s"];' % (
+ l, l, PORT_COLORS["RING"]))
+
+ vhost_labels = []
+ for p in vhosts:
+ vhost_labels.append(p["iface"]["type"] + p["iface"]["id"])
+ vhost_labels = list(set(vhost_labels))
+ for l in vhost_labels:
+ output.append(
+ '%s[label="%s", fillcolor="%s"];' % (
+ l, l, PORT_COLORS["VHOST"]))
+
+ # rank
+ output.append(
+ '{rank=same; %s}' % ("; ".join(ring_labels)))
+ if len(phys) > 0:
+ output.append(
+ '{rank=max; %s}' % (
+ phys[0]["iface"]["type"] + phys[0]["iface"]["id"]))
+ output.append(
+ '{rank=same; %s}' % ("; ".join(phy_labels)))
+
+ # subgraph
+ cluster_id = "cluster0"
+ sg_label = "Host"
+ sg_ports = "; ".join(phy_labels + ring_labels)
+ output.append(
+ 'subgraph %s {label="%s" %s}' % (cluster_id, sg_label, sg_ports))
+
+ for link in links:
+ output.append(link)
+
+ output.append("}")
+
+ # remove duplicated entries
+ f = open(output_fname, "w+")
+ f.write("\n".join(output))
+ f.close()
+
+ def to_json(self, sec_list, output_fname):
+ import json
+ f = open(output_fname, "w+")
+ f.write(json.dumps(sec_list))
+ f.close()
+
+ def to_yaml(self, sec_list, output_fname):
+ import yaml
+ f = open(output_fname, "w+")
+ f.write(yaml.dump(sec_list))
+ f.close()
+
+ def to_img(self, sec_list, output_fname):
+ tmpfile = "%s.dot" % uuid.uuid4().hex
+ self.to_dot(sec_list, tmpfile)
+ fmt = output_fname.split(".")[-1]
+ cmd = "dot -T%s %s -o %s" % (fmt, tmpfile, output_fname)
+ subprocess.call(cmd, shell=True)
+ subprocess.call("rm -f %s" % tmpfile, shell=True)
+
+ def to_http(self, sec_list):
+ tmpfile = "%s.dot" % uuid.uuid4().hex
+ self.to_dot(sec_list, tmpfile)
+ msg = open(tmpfile).read()
+ subprocess.call("rm -f %s" % tmpfile, shell=True)
+ ws_url = "ws://localhost:8989/spp_ws"
+ ws = websocket.create_connection(ws_url)
+ ws.send(msg)
+ ws.close()
+
+ def to_term(self, sec_list):
+ tmpfile = "%s.jpg" % uuid.uuid4().hex
+ self.to_img(sec_list, tmpfile)
+ from distutils import spawn
+
+ # TODO(yasufum) Add check for using only supported terminal
+
+ if spawn.find_executable("img2sixel") is not None:
+ img_cmd = "img2sixel"
+ else:
+ img_cmd = "%s/%s/imgcat.sh" % (
+ os.path.dirname(__file__), '3rd_party')
+ # Resize image to fit the terminal
+ img_size = "60%"
+ cmd = "convert -resize %s %s %s" % (img_size, tmpfile, tmpfile)
+ subprocess.call(cmd, shell=True)
+ subprocess.call("%s %s" % (img_cmd, tmpfile), shell=True)
+ subprocess.call(["rm", "-f", tmpfile])
+
+ def format_sec_status(self, stat):
+ """Return formatted secondary status as a hash
+
+ By running status command on controller, status is sent from
+ secondary process and receiving message is displayed.
+
+ This is an example of receiving status message.
+ recv:8:{Client ID 1 Idling
+ client_id:1
+ port_id:0,on,PHY,outport:2
+ port_id:1,on,PHY,outport:none
+ port_id:2,on,RING(0),outport:3
+ port_id:3,on,VHOST(1),outport:none
+ }
+
+ This method returns as following.
+ {
+ 'forward': False,
+ 'ports': [
+ {
+ 'out': 'none',
+ 'id': '0',
+ 'iface': {'type': 'PHY', 'id': '0'}
+ },
+ {
+ 'out': 'none',
+ 'id': '1',
+ 'iface': {'type': 'PHY', 'id': '1'}
+ }
+ ],
+ 'sec_id': '2'
+ }
+ """
+
+ stat_ary = stat.split("\n")
+ res = {}
+
+ try:
+ # Check running status
+ if "Idling" in stat_ary[0]:
+ res["forward"] = False
+ elif "Running" in stat_ary[0]:
+ res["forward"] = True
+ else:
+ print("Invalid forwarding status:", stat_ary[0])
+
+ ptn = re.compile(r"clinet_id:(\d+)")
+ m = ptn.match(stat_ary[1])
+ if m is not None:
+ res["sec_id"] = m.group(1)
+ else:
+ raise Exception("No client ID matched!")
+
+ ports = []
+ # match PHY, for exp. 'port_id:0,on,PHY,outport:none'
+ ptn_p = re.compile(r"port_id:(\d+),on,(\w+),outport:(\w+)")
+
+ # match RING for exp. 'port_id:2,on,RING(0),outport:3'
+ # or VHOST for exp. 'port_id:3,on,VHOST(1),outport:none'
+ ptn_v = re.compile(
+ r"port_id:(\d+),on,(\w+)\((\d+)\),outport:(\w+)")
+
+ for i in range(2, len(stat_ary)-1):
+ m = ptn_p.match(stat_ary[i])
+ if m is not None:
+ ports.append({
+ "id": m.group(1),
+ "iface": {"type": m.group(2), "id": m.group(1)},
+ "out": m.group(3)})
+ continue
+
+ m = ptn_v.match(stat_ary[i])
+ if m is not None:
+ ports.append({
+ "id": m.group(1),
+ "iface": {"type": m.group(2), "id": m.group(3)},
+ "out": m.group(4)})
+
+ res["ports"] = ports
+ return res
+
+ except Exception:
+ traceback.print_exc()
+ return None
--
2.13.1
More information about the spp
mailing list