#!/usr/bin/python
#
# Copyright (c) 2017 Mellanox Technologies. All rights reserved.
#
# This Software is licensed under one of the following licenses:
#
# 1) under the terms of the "Common Public License 1.0" a copy of which is
#    available from the Open Source Initiative, see
#    http://www.opensource.org/licenses/cpl.php.
#
# 2) under the terms of the "The BSD License" a copy of which is
#    available from the Open Source Initiative, see
#    http://www.opensource.org/licenses/bsd-license.php.
#
# 3) under the terms of the "GNU General Public License (GPL) Version 2" a
#    copy of which is available from the Open Source Initiative, see
#    http://www.opensource.org/licenses/gpl-license.php.
#
# Licensee has the right to choose one of the above licenses.
#
# Redistributions of source code must retain the above copyright
# notice and one of the license notices.
#
# Redistributions in binary form must reproduce both the above copyright
# notice, one of the license notices in the documentation
# and/or other materials provided with the distribution.
#

"""
This is a tool to parse the flow steering information.

It can either parse the output from command:

	sudo mlxdump -d /dev/mst/mt4115_pciconf0 fsdump --type FT --no_zero=true > fs_log

by doing: mlx_fs_dump.py -f fs_log

Or just run this script and it will run the command(make sure mst is running).

See ./mlx_fs_dump -h for help.
"""

__author__ =  'Bodong Wang - bodong@mellanox.com'
__author__ =  'Huy Nguyen - huyn@mellanox.com'
__author__ =  'Sunil Sudhakar Rani - sunils@mellanox.com'
__version__=  '1.0'

import sys
import os
import re
import glob
import subprocess
import argparse
import traceback
import socket
import struct
from enum import Enum
from argparse import RawTextHelpFormatter
try:
	from anytree import Node, RenderTree, NodeMixin, AsciiStyle
except ImportError:
	sys.exit("- ERR - Please install anytree (e.g, pip install anytree)")

try:
	from termcolor import colored
except ImportError:
	sys.exit("- ERR - Please install termcolor (e.g, pip install termcolor)")

# Global
tableList = []
groupList = []
entryList = []
color = 1

ftb_type = ["NIC_RX", "NIC_TX", "ESW_EGRESS_ACL","ESW_INGRESS_ACL", "ESW_FDB", "SNIFFER_RX", "SNIFFER_TX", "TX_RDMA", "TX_RDMA"]
fte_action = ["ALLOW", "DROP", "FWD", "FLOW_COUNT", "ENCAP", "DECAP", "MODIFY_HDR", "POP_VLAN", "PUSH_VLAN", "FPGA_ACC", "POP_VLAN_2", "PUSH_VLAN_2"]
fte_match = ["OUT_HDR", "MISC", "IN_HDR", "MISC_2"]
dest_type = ["VPORT", "FLOW_TABLE", "TIR", "QP"]

# Dict to rename the long name to short one
match_criteria = {
	"outer_headers.smac_47_16"		:	"smac_47_16",
	"outer_headers.smac_15_0"		:	"smac_15_0",
	"outer_headers.dmac_47_16"		:	"dmac_47_16",
	"outer_headers.dmac_15_0"		:	"dmac_15_0",
	"outer_headers.ethertype"		:	"ethtype",
	"outer_headers.first_prio"		:	"1st_prio",
	"outer_headers.first_cfi"		:	"1st_cfi",
	"outer_headers.first_vid"		:	"1st_vid",
	"outer_headers.ip_protocol"		:	"ip_prot",
	"outer_headers.ip_dscp"			:	"ip_dscp",
	"outer_headers.ip_ecn"			:	"ip_ecn",
	"outer_headers.cvlan_tag"		:	"cvlan_tag",
	"outer_headers.svlan_tag"		:	"svlan_tag",
	"outer_headers.frag"			:	"frag",
	"outer_headers.ip_version"		:	"ip_ver",
	"outer_headers.tcp_flags"		:	"tcp_flags",
	"outer_headers.tcp_sport"		:	"tcp_sport",
	"outer_headers.tcp_dport"		:	"tcp_dport",
	"outer_headers.ttl_hoplimit"		:	"ttl_hoplimit",
	"outer_headers.udp_sport"		:	"udp_sport",
	"outer_headers.udp_dport"		:	"udp_dport",
	"outer_headers.src_ip_127_96"		:	"src_ip_127_96",
	"outer_headers.src_ip_95_64"		:	"src_ip_95_64",
	"outer_headers.src_ip_63_32"		:	"src_ip_63_32",
	"outer_headers.src_ip_31_0"		:	"src_ip_31_0",
	"outer_headers.dst_ip_127_96"		:	"dst_ip_127_96",
	"outer_headers.dst_ip_95_64"		:	"dst_ip_95_64",
	"outer_headers.dst_ip_63_32"		:	"dst_ip_63_32",
	"outer_headers.dst_ip_31_0"		:	"dst_ip_31_0",

	"misc_parameters.source_sqn"		:	"src_sqn",
	"misc_parameters.src_esw_owner_vhca_id"	:	"src_esw_vhca_id",
	"misc_parameters.source_port"		:	"src_port",
	"misc_parameters.outer_second_vid"	:	"o_2nd_vid",
	"misc_parameters.outer_second_cfi"	:	"o_2nd_cfi",
	"misc_parameters.outer_second_prio"	:	"o_2nd_prio",
	"misc_parameters.inner_second_vid"	:	"i_2nd_vid",
	"misc_parameters.inner_second_cfi"	:	"i_2nd_cfi",
	"misc_parameters.inner_second_prio"	:	"i_2nd_prio",
	"misc_parameters.inner_second_svlan_ta"	:	"i_2nd_svlan_tag",
	"misc_parameters.outer_second_svlan_ta"	:	"o_2nd_svlan_tag",
	"misc_parameters.inner_second_cvlan_ta"	:	"i_2nd_cvlan_tag",
	"misc_parameters.outer_second_cvlan_ta"	:	"o_2nd_cvlan_tag",
	"misc_parameters.outer_emd_tag"		:	"o_emd_tag",
	"misc_parameters.gre_protocol"		:	"gre_protocol",
	"misc_parameters.gre_key_h"		:	"gre_key_h",
	"misc_parameters.gre_key_l"		:	"gre_key_l",
	"misc_parameters.vxlan_vni"		:	"vxlan_vni",
	"misc_parameters.geneve_oam"		:	"geneve_oam",
	"misc_parameters.geneve_vni"		:	"geneve_vni",
	"misc_parameters.outer_ipv6_flow_label"	:	"o_ipv6_flow_label",
	"misc_parameters.inner_ipv6_flow_label"	:	"i_ipv6_flow_label",
	"misc_parameters.geneve_protocol_type"	:	"geneve_prot_type",
	"misc_parameters.bth_dst_qp"		:	"bth_dst_qp",
	"misc_parameters.inner_esp_spi"		:	"i_esp_spi",
	"misc_parameters.outer_esp_spi"		:	"o_esp_spi",
	"misc_parameters.outer_emd_tag_data"	:	"o_emd_tag_data",

	"inner_headers.smac_47_16"		:	"smac_47_16",
	"inner_headers.smac_15_0"		:	"smac_15_0",
	"inner_headers.dmac_47_16"		:	"dmac_47_16",
	"inner_headers.dmac_15_0"		:	"dmac_15_0",
	"inner_headers.ethertype"		:	"ethtype",
	"inner_headers.first_prio"		:	"1st_prio",
	"inner_headers.first_cfi"		:	"1st_cfi",
	"inner_headers.first_vid"		:	"1st_vid",
	"inner_headers.ip_protocol"		:	"ip_prot",
	"inner_headers.ip_dscp"			:	"ip_dscp",
	"inner_headers.ip_ecn"			:	"ip_ecn",
	"inner_headers.cvlan_tag"		:	"cvlan_tag",
	"inner_headers.svlan_tag"		:	"svlan_tag",
	"inner_headers.frag"			:	"frag",
	"inner_headers.ip_version"		:	"ip_ver",
	"inner_headers.tcp_flags"		:	"tcp_flags",
	"inner_headers.tcp_sport"		:	"tcp_sport",
	"inner_headers.tcp_dport"		:	"tcp_dport",
	"inner_headers.ttl_hoplimit"		:	"ttl_hoplimit",
	"inner_headers.udp_sport"		:	"udp_sport",
	"inner_headers.udp_dport"		:	"udp_dport",
	"inner_headers.src_ip_127_96"		:	"src_ip_127_96",
	"inner_headers.src_ip_95_64"		:	"src_ip_95_64",
	"inner_headers.src_ip_63_32"		:	"src_ip_63_32",
	"inner_headers.src_ip_31_0"		:	"src_ip_31_0",
	"inner_headers.dst_ip_127_96"		:	"dst_ip_127_96",
	"inner_headers.dst_ip_95_64"		:	"dst_ip_95_64",
	"inner_headers.dst_ip_63_32"		:	"dst_ip_63_32",
	"inner_headers.dst_ip_31_0"		:	"dst_ip_31_0",
}

# Dict to find the ethType name
eth_type = {
	"0x800" : "IPv4",
	"0x806" : "APR",
	"33024" : "VLAN_TAGGED",
	"0x8914": "FCOE",
	"0x86dd": "IPv6",
	"0x8915": "RCOE"
}

# Dict to find the IP_Protocal name
ip_proto = {
	1 : "ICMP",
	2 : "IGMP",
	4 : "IP-in-IP",
	6  : "TCP",
	17 : "UDP",
	41 : "IPv6",
	50 : "ESP",
	51 : "AH",
}

def ip2int(addr):
	return struct.unpack("!I", socket.inet_aton(addr))[0]

def int2ip(addr):
	return socket.inet_ntoa(struct.pack("!I", addr))

def test_bit(node=[], bit=None):
	try:
		return node[node.index(bit) + 1]
	except ValueError:
		return "0"

def green(string):
	if color:
    		return colored(string, 'green')
	else:
		return string

def blue(string):
	if color:
    		return colored(string, 'blue')
	else:
		return string

def red(string):
	if color:
    		return colored(string, 'red')
	else:
		return string

class flow_table(NodeMixin):
	def __init__(self, parent=None, table_id=None, level=None, type=None, table_miss_id=None, rootFlag=1):
		self.parent = parent
		self.table_id = table_id
		self.vport = 0
		self.level = level
		self.type = type # Use flow_table_type
		self.table_miss_id = table_miss_id
		self.rootFlag = rootFlag

class flow_group(NodeMixin):
	def __init__(self, parent=None, table_id=None, group_id=None, match_enable=None, match_cr=[]):
		self.parent = parent
		self.table_id = table_id
		self.group_id = group_id
		self.match_enable = match_enable
		self.cr_found = []

		for i, cr in enumerate(match_cr):
			# Only check the cr name
			if not i % 2:
				try:
					cr_rename = match_criteria[cr]
				except KeyError:
					continue
				# Save cr name
				self.cr_found.append(cr_rename)
				# Save cr value
				self.cr_found.append(match_cr[i+1])

class flow_entry(NodeMixin):
	def __init__(self, parent=None, table_id=None,\
			group_id=None, action=None, flow_index=None,\
			dst_type=None, dst_value=None, match_cr=[]):
		self.parent = parent
		self.table_id = table_id
		self.group_id = group_id
		self.action = action
		self.flow_index = flow_index
		self.dst_type = dst_type
		self.dst_value = dst_value
		self.cr_found = []

		for i, cr in enumerate(match_cr):
			# Only check the cr name
			if not i % 2:
				try:
					cr_rename = match_criteria[cr]
				except KeyError:
					continue
				# Save cr name
				self.cr_found.append(cr_rename)
				# Save cr value
				self.cr_found.append(match_cr[i+1])

def printFt(node):
	return ("level: %s, type: %s" % (node.level, node.type))

def printFg(node):
	ret = node.match_enable
	pad = ": |"

	for i, cr in enumerate(node.cr_found):
		# Only print cr with none 0 value
		if (i % 2) and (cr is not "0"):
			cr_name = node.cr_found[i-1]
			ret += pad + cr_name + "|"
			pad = ""

	return ret

def printFte(node):
	ret = " "
	is_ipv4 = False

	if node.dst_type is not "0":
		ret += "to (" + node.dst_type + ":" + node.dst_value + ") "

	for i, cr in enumerate(node.cr_found):
		# Only print cr with none 0 value
		if (i % 2) and (cr is not "0"):
			cr_name = node.cr_found[i-1]
			if "ethtype" in cr_name:
				try:
					cr = eth_type[cr]
					if cr is "IPv4":
						is_ipv4 = True
				except KeyError:
					pass
			elif "ip_prot" in cr_name:
				try:
					cr = ip_proto[int(cr, 0)]
				except KeyError:
					pass
			elif "dmac_47_16" in cr_name:
				cr_name = "dmac"
				try:
					index = node.cr_found.index("dmac_15_0")
				except ValueError:
					index = -1

				if index is not -1:
					cr = cr.zfill(8) + node.cr_found[index+1].lstrip("0x").zfill(4)
					node.cr_found[index] = "0"
					node.cr_found[index+1] = "0"
			elif "smac_47_16" in cr_name:
				cr_name = "smac"
				try:
					index = node.cr_found.index("smac_15_0")
				except ValueError:
					index = -1

				if index is not -1:
					cr = cr.zfill(8) + node.cr_found[index+1].lstrip("0x").zfill(4)
					node.cr_found[index] = "0"
					node.cr_found[index+1] = "0"
			elif ("_ip_31_0" in cr_name) and is_ipv4:
				try:
					cr = int2ip(int(cr, 16))
				except KeyError:
					pass

			ret += cr_name + ":" + cr + " "

	return ret

def printNode(pre, node, substring):
	if isinstance(node, flow_table):
		print(blue("%s%s%s (%s)") % (pre, "FT: ", node.table_id, printFt(node)))
	elif isinstance(node, flow_group):
 		print(red("%s%s%s (%s)") % (pre, "FG: ", node.group_id, printFg(node)))
	elif isinstance(node, flow_entry):
		print(green("%s%s%s (%s)%s") % \
			(pre, "FTE: ", node.flow_index, node.action, printFte(node)))

class fancy_dump():

	def __init__(self, args=[]):
		global color
		parser = argparse.ArgumentParser(\
			description='-'*65 + "\n" +
			'Dump flow table in a fancy way.\n\n' + \
			'Package/Tool required:\n' +\
			'\t - mst\n' +\
			'\t - python packages (anytree, termcolor)\n' + \
			'-'*65,
			formatter_class=RawTextHelpFormatter)
		# Add arguments
		parser.add_argument('-d', help='Device name from #mst status, currently supports ConnectX-4 (/dev/mst/mt4115_pciconf) and ConnectX-4 LX (/dev/mst/mt4117_pciconf)', dest='device', required=True, default=None)
		parser.add_argument('-g', type=int, help='Gvmi number', dest='gvmi', required=False, default=0)
		parser.add_argument('-c', type=int, help='Print with color', dest='color', required=False, default=1)
		parser.add_argument('-f', help='Parse a file from mlxdump', dest='in_file', required=False, default=None)
		# Array for all arguments passed to script
		arg = parser.parse_args()
		self.gvmi = arg.gvmi
		color = arg.color
		self.device = arg.device
		self.in_file = arg.in_file

	def run(self):
		if self.in_file is None:
			#run fsdump command
			self.cmd = "sudo mlxdump -d " + str(self.device) + " fsdump --type FT --no_zero" + " --gvmi " + str(self.gvmi)

			p = subprocess.Popen(self.cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
			out, err = p.communicate()

			if err:
				return ("[%s] Failed\n- ERROR - %s" % (self.cmd, err))

			if b"-E-" in out:
				return ("[%s] Failed\n- %s" % (self.cmd, out))
		else:
			with open(self.in_file, 'rb') as f:
				    out = f.read()

		# split the output with empty lines
		out_1 = out.decode('utf8').split(os.linesep + os.linesep)

		# split with ,
		for i in out_1:
			node = i.replace("\n", ",").replace("-", "").replace(" ", "").replace("=", ",").replace(":", ",").lstrip(',').split(',')
			#print node

			table_id = test_bit(node, "table_id")
			group_id = test_bit(node, "group_id")

			if node[0] == "FT":
				vport = test_bit(node, "gvmi")
				type = ftb_type[int(test_bit(node, "table_type"), 0)]
				level = test_bit(node, "level")
				table_miss_id = test_bit(node, "table_miss_id")

				tableList.append(flow_table(None, table_id, level, type, table_miss_id, 1))

			if node[0] == "FG":
				match_enable_int = int(test_bit(node, "match_criteria_enable"), 0)
				match_enable = "NO_MATCH"
				pad = ""

				if match_enable_int:
					match_cr = node[node.index("match_criteria_enable") + 2:]
				else:
					match_cr = []

				for i in range(0, len(fte_match)):
					if (match_enable_int & (1 << i)):
						if match_enable is "NO_MATCH":
							match_enable = ""
						match_enable  = match_enable + pad + fte_match[i]
						pad = " "

				groupList.append(flow_group(None, table_id, group_id, match_enable, match_cr))

			if node[0] == "FTE":
				action_int = int(test_bit(node, "action"), 0)
				action = ""
				pad = ""
				for i in range(0, len(fte_action)):
					if (action_int & (1 << i)):
						action = action + pad + fte_action[i]
						pad = " "
				valid = test_bit(node,"valid")
				flow_index = test_bit(node,"flow_index")
				dst_value = test_bit(node,"destination[0].destination_id")
				dst_type = test_bit(node, "destination[0].destination_type").\
						split("(")[0].rstrip('_')

				if int(valid, 0):
					match_cr = node[node.index("valid") + 2:]
				else:
					match_cr = []
				entryList.append(flow_entry(None, table_id,\
						group_id, action, flow_index,\
						dst_type, dst_value, match_cr))

		for ftb_idx, ftb in enumerate(tableList):
                        for fg_idx, fg in enumerate(groupList):
                            for fte_idx, fte in enumerate(entryList):
                                if int(fte.group_id, 0) == int(fg.group_id, 0):
                                    entryList[fte_idx].parent = groupList[fg_idx]
                            if int(ftb.table_id, 0) == int(fg.table_id, 0):
                                groupList[fg_idx].parent = tableList[ftb_idx]

		for fte_idx, fte in enumerate(entryList):
			if fte.dst_type == "FLOW_TABLE":
				for ftb_idx, ftb in enumerate(tableList):
					if int(ftb.table_id, 0) == int(fte.dst_value, 0):
						tableList[ftb_idx].parent = entryList[fte_idx]
						tableList[ftb_idx].rootFlag = 0

		for i in tableList:
			if not i.rootFlag:
				continue

			for pre, _, node in RenderTree(i, style=AsciiStyle()):
				printNode(pre, node, 0)

if __name__ == "__main__":
	test = fancy_dump(sys.argv[1:])
	rc = test.run()
	sys.exit(rc)
