Add ECMP calculator tool (#12482) (#13301)

This commit is contained in:
mssonicbld 2023-01-09 00:48:56 +08:00 committed by GitHub
parent e89456b3d9
commit 1e522ff3a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1371 additions and 0 deletions

View File

@ -32,6 +32,7 @@ RUN apt-get update && \
python3-pip \
python3-dev \
python-is-python3 \
python3-jsonschema \
{%- if ENABLE_ASAN == "y" %}
libasan6 \
{%- endif %}
@ -70,4 +71,10 @@ RUN mkdir -p /etc/supervisor/conf.d/
RUN sonic-cfggen -a "{\"ENABLE_ASAN\":\"{{ENABLE_ASAN}}\"}" -t /usr/share/sonic/templates/supervisord.conf.j2 > /etc/supervisor/conf.d/supervisord.conf
RUN rm -f /usr/share/sonic/templates/supervisord.conf.j2
RUN mkdir -p /usr/lib/ecmp_calc
COPY ["ecmp_calculator/ecmp_calc.py", "/usr/bin"]
COPY ["ecmp_calculator/ecmp_calc_sdk.py", "/usr/lib/ecmp_calc"]
COPY ["ecmp_calculator/packet_scheme.py", "/usr/lib/ecmp_calc"]
COPY ["lib/port_utils.py", "/usr/lib"]
ENTRYPOINT ["/usr/local/bin/supervisord"]

View File

@ -0,0 +1,136 @@
# SONiC ECMP Calculator
## Description
An equal cost multipath (ECMP) is formed when routing table contains multiple next hop addresses for the same destination IP address.
ECMP will load balance the outbound traffic between the IP interfaces.
The purpose of ECMP calculator is to calculate which IP interface ECMP will choose and return the physical interface packet will egress from.
Packet is defined by a JSON file given as an argument to the tool.
## Usage notes
1. ECMP calculator performs its calculations based on the current operational state of the router. In order to calculate the egress port, it fetches routes from HW. Routes exist in HW only for next hops with a resolved ARP.
2. ECMP calculator supports only routed packets.
- IPv4/IPv6 TCP/UDP packets
- IPinIP and VXLAN encapsulated packets
3. Changes done in the packet classification (e.g. ACL, PBR) are not taken into consideration during calculation.
## Command line interface
1. User shall provide the following input parameters:
- JSON file describing a packet
- Ingress port (e.g. "Ethernet0", must pe a physical interface)
- Debug option for debug purposes (optional)
- VRF name (optional)
2. Usage example:
```
$ show ip ecmp-egress-port --packet /tmp/packet.json --ingress-port Ethernet0 --vrf Vrf_red --debug
Egress port: Ethernet4
```
ECMP calculator is a vendor specific tool. If tool was not implemented and CLI command is being called, the following message will be returned to user:
```
$ show ip ecmp-egress-port --packet /tmp/packet.json --ingress-port Ethernet0 --vrf Vrf_red --debug
ECMP calculator is not available in this image
```
## Packet JSON
1. Numbers in packet JSON must be in base-ten.
2. For packets with single header, outer header shall be provided.
3. The following table defines the structure of a packet JSON file.
| ecmp_hash | | | | | | |
|-----------|-------------|-------|-------------|-------------|--------|----------------------------------------------------------------------------------|
| | packet_info | | | | object | |
| | | outer | | | object | |
| | | | layer2 | | object | |
| | | | | smac | string | |
| | | | | dmac | string | |
| | | | | ethertype | number | 16bits, needed for IPv4 or IPv6 packet |
| | | | | outer_vid | number | 12bits |
| | | | | outer_pcp | number | 3bits |
| | | | | outer_dei | number | 1bits |
| | | | | inner_vid | number | QinQ |
| | | | | inner_pcp | number | QinQ |
| | | | | inner_dei | number | QinQ |
| | | | ipv4 | | object | |
| | | | | sip | string | |
| | | | | dip | string | |
| | | | | proto | number | 8bits |
| | | | | dscp | number | 6bits |
| | | | | ecn | number | 2bits |
| | | | | mflag | number | 1bit |
| | | | | l3_length | number | 16bits |
| | | | ipv6 | | object | should not co-exist with ipv4 field |
| | | | | sip | string | |
| | | | | dip | string | |
| | | | | mflag | number | 1bit |
| | | | | next_header | number | 8bits |
| | | | | dscp | number | 6bits |
| | | | | ecn | number | 2bits |
| | | | | l3_length | number | 16bits |
| | | | | flow_label | number | 20bits |
| | | | tcp_udp | | object | |
| | | | | sport | number | 16bits |
| | | | | dport | number | 16bits |
| | | | vxlan_nvgre | | object | |
| | | | | vni | number | 24bits |
| | | inner | | | object | overlay |
| | | | layer2 | | object | |
| | | | | smac | string | |
| | | | | dmac | string | |
| | | | | ethertype | number | 16bits |
| | | | ipv4 | | object | |
| | | | | sip | string | |
| | | | | dip | string | |
| | | | | mflag | number | 1bit |
| | | | | proto | number | 8bits |
| | | | ipv6 | | object | should not co-exist with ipv4 field |
| | | | | sip | string | |
| | | | | dip | string | |
| | | | | mflag | number | 1bit |
| | | | | next_header | number | 8bits |
| | | | | flow_label | number | 20bits |
| | | | tcp_udp | | object | |
| | | | | sport | number | 16bits |
| | | | | dport | number | 16bits |
4. Packet JSON file example
```json
{
"packet_info": {
"outer": {
"ipv4": {
"sip": "10.10.10.10",
"dip": "3.3.3.3",
"proto": 17
},
"layer2": {
"smac": "24:8a:07:1e:82:ed",
"dmac": "1c:34:da:1c:a1:00",
"ethertype": 2048
},
"tcp_udp": {
"sport": 100,
"dport": 4789
},
"vxlan_nvgre": {
"vni": 100
}
},
"inner": {
"layer2": {
"smac": "11:11:11:11:11:11",
"dmac": "22:22:22:22:22:22",
"ethertype": 2048
},
"ipv4": {
"sip": "1.1.1.1",
"dip": "2.2.2.3",
"proto": 17,
"mflag": 0
},
"tcp_udp": {
"sport": 100,
"dport": 200
}
}
}
}

View File

@ -0,0 +1,510 @@
#!/usr/bin/env python3
import json, jsonschema
import argparse
import ipaddress
import re
import subprocess
import pprint
import os
import sys
usr_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
lib_path = os.path.join(usr_path, "lib")
ecmp_lib_path = os.path.join(lib_path, "ecmp_calc")
sys.path.append(lib_path)
sys.path.append(ecmp_lib_path)
from ecmp_calc_sdk import sx_open_sdk_connection, sx_get_active_vrids, sx_router_get_ecmp_id, \
sx_router_ecmp_nexthops_get, sx_get_router_interface, \
sx_port_vport_base_get, sx_router_neigh_get_mac, sx_fdb_uc_mac_addr_get, \
sx_lag_port_group_get, sx_make_ip_prefix_v4, sx_make_ip_prefix_v6, \
sx_vlan_ports_get, sx_ip_addr_to_str, sx_close_sdk_connection, \
PORT, VPORT, VLAN, SX_ENTRY_NOT_FOUND
from packet_scheme import PACKET_SCHEME
from port_utils import sx_get_ports_map, is_lag
IP_VERSION_IPV4 = 1
IP_VERSION_IPV6 = 2
PORT_CHANNEL_IDX = 1
VRF_NAME_IDX = 1
IP_VERSION_MAX_MASK_LEN = {IP_VERSION_IPV4: 32, IP_VERSION_IPV6: 128}
INTF_TABLE = 'INTF_TABLE'
HASH_CALC_PATH = '/usr/bin/sx_hash_calculator'
HASH_CALC_INPUT_FILE = "/tmp/hash_calculator_input.json"
HASH_CALC_OUTPUT_FILE = "/tmp/hash_calculator_output.json"
def exec_cmd(cmd):
""" Execute shell command """
return subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=False).decode("utf-8")
def is_mac_valid(mac):
return bool(re.match("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", mac))
def is_ip_valid(address, ip_version):
try:
if ip_version == IP_VERSION_IPV4:
ip = ipaddress.IPv4Address(address)
invalid_list = ['0.0.0.0','255.255.255.255']
else:
ip = ipaddress.IPv6Address(address)
invalid_list = ['0::0']
if ip.is_link_local:
print ("Link local IP {} is not valid".format(ip))
return False
if ip in invalid_list:
print ("IP {} is not valid".format(ip))
return False
if ip.is_multicast:
print ("Multicast IP {} is not valid".format(ip))
return False
if ip.is_loopback:
print ("Loopback IP {} is not valid".format(ip))
return False
except ipaddress.AddressValueError:
return False
return True
def load_json(filename):
data = None
with open(filename) as f:
try:
data = json.load(f)
except json.JSONDecodeError as e:
raise ValueError("Failed to load JSON file '{}', error: '{}'".format(filename, e))
return data
def create_network_addr(ip_addr, mask_len, ip_version):
ip_addr_mask = "{}/{}".format(ip_addr, mask_len)
if ip_version == IP_VERSION_IPV4:
network_addr = ipaddress.IPv4Network(ip_addr_mask,strict = False)
else:
network_addr = ipaddress.IPv6Network(ip_addr_mask,strict = False)
network_addr_ip = network_addr.with_netmask.split('/')[0]
network_addr_mask = network_addr.with_netmask.split('/')[1]
if ip_version == IP_VERSION_IPV4:
network_addr = sx_make_ip_prefix_v4(network_addr_ip, network_addr_mask)
else:
network_addr = sx_make_ip_prefix_v6(network_addr_ip, network_addr_mask)
return network_addr
class EcmpCalcExit(Exception):
pass
class EcmpCalc:
def __init__(self):
self.packet = {}
self.ports_map = {}
self.ecmp_ids = {}
self.next_hops = {}
self.user_vrf = ''
self.ingress_port = ""
self.egress_ports = []
self.debug = False
self.open_sdk_connection()
self.init_ports_map()
self.get_active_vrids()
def __del__(self):
self.close_sdk_connection()
self.cleanup()
def cleanup(self):
for filename in [HASH_CALC_INPUT_FILE, HASH_CALC_OUTPUT_FILE]:
if os.path.exists(filename):
os.remove(filename)
def close_sdk_connection(self):
sx_close_sdk_connection(self.handle)
def open_sdk_connection(self):
self.handle = sx_open_sdk_connection()
def debug_print(self, *args, **kwargs):
if self.debug == True:
print(*args, **kwargs)
def init_ports_map(self):
self.ports_map = sx_get_ports_map(self.handle)
def validate_ingress_port(self, interface):
if interface not in self.ports_map.values():
raise ValueError("Invalid interface {}".format(interface))
self.ingress_port = interface
def validate_args(self, interface, packet, vrf, debug):
if (debug is True):
self.debug = True
self.validate_ingress_port(interface)
self.validate_packet_json(packet)
if (vrf is not None):
self.user_vrf = vrf
if not self.validate_vrf():
raise ValueError("VRF validation failed: VRF {} does not exist".format(self.user_vrf))
def validate_vrf(self):
query_output = exec_cmd(['/usr/bin/redis-cli', '-n', '0', 'keys','*VRF*']).strip()
if not query_output:
return False
vrf_entries= query_output.split('\n')
for entry in vrf_entries:
vrf = entry.split(':')[VRF_NAME_IDX]
if vrf == self.user_vrf:
return True
return False
def get_ecmp_id(self):
ip_addr = self.dst_ip
ip_version = self.ip_version
max_mask_len = IP_VERSION_MAX_MASK_LEN[self.ip_version]
route_found = False
for vrid in self.vrid_list:
for mask_len in range(max_mask_len, 0, -1):
network_addr = create_network_addr(ip_addr, mask_len, ip_version)
ecmp_id = sx_router_get_ecmp_id(self.handle, vrid, network_addr)
if ecmp_id != SX_ENTRY_NOT_FOUND:
route_found = True
self.debug_print("Found route for destination IP {} ECMP id {} VRID {}".format(self.dst_ip, ecmp_id, vrid))
self.ecmp_ids[vrid] = ecmp_id
# move to next vrid
break
if not route_found:
raise EcmpCalcExit("No route found for given packet")
def get_next_hops(self):
next_hops = []
ecmp_found = False
for vrid in self.ecmp_ids.keys():
ecmp_id = self.ecmp_ids[vrid]
next_hops = sx_router_ecmp_nexthops_get(self.handle, ecmp_id)
if len(next_hops) > 1:
if self.debug:
next_hops_ips = []
for nh in next_hops:
ip = nh.next_hop_key.next_hop_key_entry.ip_next_hop.address
next_hops_ips.append(sx_ip_addr_to_str(ip))
print("Next hops IPs {}, VRID {}".format(next_hops_ips, vrid))
print("Found ECMP for destination IP {} ECMP id {}, now checking if port is member in VRF {}".
format(self.dst_ip, ecmp_id, 'default' if self.user_vrf=='' else self.user_vrf))
self.next_hops[vrid] = next_hops
ecmp_found = True
if not ecmp_found:
raise EcmpCalcExit("No ECMP for given packet")
def calculate_egress_port(self):
for vrid in self.vrid_list:
if vrid not in self.next_hops.keys():
continue
next_hops = self.next_hops[vrid]
next_hop_idx = self.get_next_hop_index(len(next_hops))
next_hop = next_hops[next_hop_idx]
rif = next_hop.next_hop_key.next_hop_key_entry.ip_next_hop.rif
ip = next_hop.next_hop_key.next_hop_key_entry.ip_next_hop.address
rif_params = sx_get_router_interface(self.handle, vrid, rif)
self.debug_print("Next hop ip to which trafic will egress: {}".format(sx_ip_addr_to_str(ip)))
# Handle router port
if PORT in rif_params:
logical = rif_params[PORT]
port_type = PORT
vlan_id = 0
# Handle vlan subinterface
elif VPORT in rif_params:
logical, vlan_id = sx_port_vport_base_get(self.handle, rif_params[VPORT])
port_type = VPORT
# Handle vlan interface
elif VLAN in rif_params:
vlan_id = rif_params[VLAN]
neigh_mac = sx_router_neigh_get_mac(self.handle, rif, ip)
if neigh_mac is not None:
mac_entry = sx_fdb_uc_mac_addr_get(self.handle, vlan_id, neigh_mac)
if mac_entry is not None:
logical = mac_entry.log_port
port_type = VLAN
# Handle flood case
if (neigh_mac is None) or (mac_entry is None):
vlan_members = sx_vlan_ports_get(self.handle, rif_params[VLAN])
for port in vlan_members:
if is_lag(port):
port = self.get_lag_member(port, True)
self.egress_ports.append(self.ports_map[port])
return
# Check if port is binded to VRF we got from the user
if is_lag(logical):
lag_logical = logical
logical = self.get_lag_member(lag_logical)
egress_port = self.ports_map[logical]
port_channel = self.get_port_channel_name(egress_port)
if self.is_port_bind_to_user_vrf(port_type, port_channel, vlan_id):
self.egress_ports.append(egress_port)
return
else:
egress_port = self.ports_map[logical]
if self.is_port_bind_to_user_vrf(port_type, egress_port, vlan_id):
self.egress_ports.append(egress_port)
return
def print_egress_port(self):
if len(self.egress_ports) == 0:
print("Egress port not found, check input parameters")
elif len(self.egress_ports) == 1:
print("Egress port: {}".format(self.egress_ports[0]))
else:
egress_ports = ''
for port in self.egress_ports:
egress_ports += ' ' + port
print("Egress ports:{}".format(egress_ports))
def is_port_bind_to_user_vrf(self, port_type, port, vlan_id = 0):
if port_type == PORT:
# INTF_TABLE:Ethernet0
entry = '{}:{}'.format(INTF_TABLE, port)
elif port_type == VPORT:
# INTF_TABLE:Ethernet0.300
entry = '{}:{}.{}'.format(INTF_TABLE, port, vlan_id)
elif port_type == VLAN:
# INTF_TABLE:Vlan300
entry = '{}:Vlan{}'.format(INTF_TABLE, vlan_id)
port_vrf = exec_cmd(['/usr/bin/redis-cli', '-n', '0', 'hget', entry, 'vrf_name'])
if self.user_vrf == port_vrf.strip():
return True
return False
# Get port-channel name for given port-channel member port
def get_port_channel_name(self, port):
query_output = exec_cmd(['/usr/bin/redis-cli', '-n', '0', 'keys','*LAG_MEMBER_TABLE*'])
for line in query_output.split('\n'):
if str(port) in line:
port_channel = line.split(':')[PORT_CHANNEL_IDX]
return port_channel
raise KeyError("Failed to get port-channel name for interface {}".format(port))
def get_ingress_port_logical_idx(self):
for logical_index, sonic_port_name in self.ports_map.items():
if sonic_port_name == self.ingress_port:
return logical_index
raise KeyError("Failed to get logical index for interface {}".format(self.ingress_port))
# Get index in next hop array from which packet will egress
def get_next_hop_index(self, ecmp_size):
logical = self.get_ingress_port_logical_idx()
ecmp_hash = {
"ingress_port": str(hex(logical)),
"packet_info":self.packet['packet_info'],
"ecmp_size": ecmp_size,
}
self.debug_print("Calling hash calculator for ECMP")
hash_result = self.call_hash_calculator({'ecmp_hash': ecmp_hash})
ecmp_hash_result = hash_result['ecmp_hash']
index = ecmp_hash_result['ecmp_index']
self.debug_print("Next hop index to which trafic will egress: {}".format(index))
return index
# Get index in LAG memebrs array from which packet will egress
def get_lag_member_index(self, lag_size, flood_case = False):
logical = self.get_ingress_port_logical_idx()
lag_hash = {
"ingress_port": str(hex(logical)),
"packet_info": self.packet['packet_info'],
"lag_size": lag_size,
}
self.debug_print("Calling hash calculator for LAG, flood case {}".format(True if flood_case else False))
hash_result = self.call_hash_calculator({"lag_hash": lag_hash})
lag_hash_result = hash_result["lag_hash"]
if flood_case:
index = lag_hash_result['lag_mc_index']
else:
index = lag_hash_result['lag_index']
self.debug_print("Lag member index from which trafic will egress: {}".format(index))
return index
# Get LAG memebr from which packet will egress
def get_lag_member(self, logical, flood_case = False):
lag_members = sx_lag_port_group_get(self.handle, logical)
lag_members.sort()
member_index = self.get_lag_member_index(len(lag_members), flood_case)
lag_member = lag_members[member_index]
self.debug_print("Lag member from which trafic will egress: {}".format(lag_member))
return lag_member
def call_hash_calculator(self, input_dict):
with open(HASH_CALC_INPUT_FILE, "w") as outfile:
json.dump(input_dict, outfile)
out = exec_cmd([HASH_CALC_PATH, '-c', HASH_CALC_INPUT_FILE, '-o', HASH_CALC_OUTPUT_FILE, '-d'])
self.debug_print ("Hash calculator output:\n{}".format(out))
with open(HASH_CALC_OUTPUT_FILE, 'r') as openfile:
output_dict = json.loads(openfile.read())
return output_dict
def get_active_vrids(self):
self.vrid_list = sx_get_active_vrids(self.handle)
def validate_ipv4_header(self, header):
for ip in ['sip', 'dip']:
if ip in header and is_ip_valid(header[ip], IP_VERSION_IPV4) == False:
raise ValueError("Json validation failed: invalid IP {}".format(header[ip]))
def validate_ipv6_header(self, header):
for ip in ['sip', 'dip']:
if ip in header and is_ip_valid(header[ip], IP_VERSION_IPV6) == False:
raise ValueError("Json validation failed: invalid IP {}".format(header[ip]))
def validate_layer2_header(self, header):
for mac in ['smac', 'dmac']:
if mac in header and is_mac_valid(header[mac]) == False:
raise ValueError("Json validation failed: invalid mac {}".format(header[mac]))
def validate_header(self, header, is_outer_header=False):
ipv4_header = False
ipv6_header = False
# Verify IPv4 and IPv6 headers do not co-exist in header
if 'ipv4' in header:
ipv4_header = True
if 'ipv6' in header:
ipv6_header = True
if ipv4_header and ipv6_header:
raise ValueError("Json validation failed: IPv4 and IPv6 headers can not co-exist")
if ipv4_header:
# Verify valid IPs in header
self.validate_ipv4_header(header['ipv4'])
if is_outer_header:
if 'dip' not in header['ipv4']:
raise ValueError("Json validation failed: destination IP is mandatory")
self.dst_ip = header['ipv4']['dip']
self.ip_version = IP_VERSION_IPV4
if 'tcp_udp' in header and 'proto' not in header['ipv4']:
raise ValueError("Json validation failed: transport protocol (proto) is mandatory when transport layer port exists")
elif ipv6_header:
self.validate_ipv6_header(header['ipv6'])
if is_outer_header:
if 'dip' not in header['ipv6']:
raise ValueError("Json validation failed: destination IP is mandatory")
self.dst_ip = header['ipv6']['dip']
self.ip_version = IP_VERSION_IPV6
if 'tcp_udp' in header and 'next_header' not in header['ipv6']:
raise ValueError("Json validation failed: transport protocol (next_header) is mandatory when transport layer port exists")
# Verify valid macs in header
if header['layer2']:
self.validate_layer2_header(header['layer2'])
def validate_outer_header(self):
outer_header = self.packet['packet_info'].get('outer')
if not outer_header:
raise ValueError("Json validation failed: outer header is mandatory")
self.validate_header(outer_header, is_outer_header=True)
def validate_inner_header(self):
inner_header = self.packet['packet_info'].get('inner')
if not inner_header:
return
self.validate_header(inner_header)
def validate_packet_json(self, packet_json):
# Verify json has valid format
self.packet = load_json(packet_json)
# Verify json schema
try:
jsonschema.validate(self.packet, PACKET_SCHEME)
except jsonschema.exceptions.ValidationError as e:
raise ValueError("Json validation failed: {}".format(e))
# Verify outer header
self.validate_outer_header()
# Verify inner header
self.validate_inner_header()
if self.debug:
print('Packet:')
pprint.pprint(self.packet)
def main():
rc = 0
try:
parser = argparse.ArgumentParser(description="ECMP calculator")
parser.add_argument("-i", "--interface", required=True, help="Ingress interface")
parser.add_argument("-p", "--packet", required=True, help="Packet description")
parser.add_argument("-v", "--vrf", help="VRF name")
parser.add_argument("-d", "--debug", default=False, action="store_true", help="Flag for debug")
args = parser.parse_args()
ecmp_calc = EcmpCalc()
ecmp_calc.validate_args(args.interface, args.packet, args.vrf, args.debug)
ecmp_calc.get_ecmp_id()
ecmp_calc.get_next_hops()
ecmp_calc.calculate_egress_port()
ecmp_calc.print_egress_port()
except EcmpCalcExit as s:
print(s)
except ValueError as s:
print("Value error: {}".format(s))
rc = 1
except Exception as s:
print("Error: {}".format(s))
rc = 2
return rc
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,456 @@
#!/usr/bin/env python3
from python_sdk_api.sx_api import *
from port_utils import sx_check_rc, SWITCH_ID
import struct
import socket
import ctypes
import sys
PORT='port'
VPORT='vport'
VLAN='vlan'
SX_ENTRY_NOT_FOUND = -1
SDK_DEFAULT_LOG_LEVEL=SX_VERBOSITY_LEVEL_ERROR
def severity_to_verbosity(severity):
value = severity + 1
verbosity = 0
while not (value & 1):
value = value >> 1
verbosity = verbosity + 1
return verbosity
def log_cb(severity, module_name, msg):
verbosity = severity_to_verbosity(severity)
log_level = {SX_VERBOSITY_LEVEL_INFO: 'LOG_INFO', SX_VERBOSITY_LEVEL_ERROR: 'LOG_ERR',
SX_VERBOSITY_LEVEL_WARNING: 'LOG_WARN', SX_VERBOSITY_LEVEL_DEBUG: 'LOG_DEBUG',
SX_VERBOSITY_LEVEL_NOTICE: 'LOG_NOTICE'}
if verbosity in log_level.keys():
level = log_level[verbosity]
else:
level = 'LOG'
if verbosity <= SDK_DEFAULT_LOG_LEVEL:
print('{}: {}'.format(level, ctypes.cast(msg,ctypes.c_char_p).value.decode('utf-8')))
def sx_open_sdk_connection():
""" Open connection to SDK
Args:
None
Returns:
SDK handle
"""
sx_api = ctypes.CDLL('libsxapi.so', mode=ctypes.RTLD_GLOBAL)
log_cb_type = ctypes.CFUNCTYPE(None, ctypes.c_int,
ctypes.POINTER(ctypes.c_char),
ctypes.POINTER(ctypes.c_char))
log_cb_fn = log_cb_type(log_cb)
handle_p = ctypes.pointer(ctypes.c_int64())
rc = sx_api.sx_api_open(log_cb_fn, handle_p)
sx_check_rc(rc)
return handle_p.contents.value
def sx_close_sdk_connection(handle):
""" Close connection to SDK
Args:
SDK handle
"""
rc = sx_api_close(handle)
sx_check_rc(rc)
def sx_get_active_vrids(handle):
""" Get existing virtual router IDs
Args:
handle (sx_api_handle_t): SDK handle
Returns:
list : List of virtual routers ids
"""
try:
vrid_list = []
vrid_cnt_p = new_uint32_t_p()
uint32_t_p_assign(vrid_cnt_p, 0)
vrid_key_p = new_sx_router_id_t_p()
sx_router_id_t_p_assign(vrid_key_p, 0)
vrid_key = sx_router_id_t_p_value(vrid_key_p)
rc = sx_api_router_vrid_iter_get(handle, SX_ACCESS_CMD_GET, vrid_key, None, None, vrid_cnt_p)
sx_check_rc(rc)
vrid_cnt = uint32_t_p_value(vrid_cnt_p)
vrid_list_p = new_sx_router_id_t_arr(vrid_cnt)
rc = sx_api_router_vrid_iter_get(handle, SX_ACCESS_CMD_GET_FIRST, vrid_key, None, vrid_list_p, vrid_cnt_p)
sx_check_rc(rc)
vrid_cnt = uint32_t_p_value(vrid_cnt_p)
for i in range(0, vrid_cnt):
vrid = sx_router_id_t_arr_getitem(vrid_list_p, i)
vrid_list.append(vrid)
return vrid_list
finally:
delete_sx_router_id_t_arr(vrid_list_p)
delete_sx_router_id_t_p(vrid_key_p)
delete_uint32_t_p(vrid_cnt_p)
def sx_router_get_ecmp_id(handle, vrid, ip_prefix):
""" Get ECMP id for a given IP prefix
Args:
handle (sx_api_handle_t): SDK handle
vrid (sx_router_id_t): Virtual router id
ip_prefix (sx_ip_prefix_t): Network address
Returns:
int: ECMP id
"""
try:
ip_prefix_p = new_sx_ip_prefix_t_p()
sx_ip_prefix_t_p_assign(ip_prefix_p, ip_prefix)
entries_cnt_p = new_uint32_t_p()
uint32_t_p_assign(entries_cnt_p, 1)
entries_array = new_sx_uc_route_get_entry_t_arr(1)
rc = sx_api_router_uc_route_get(handle, SX_ACCESS_CMD_GET, vrid, ip_prefix_p, None, entries_array, entries_cnt_p)
if rc == SX_STATUS_ENTRY_NOT_FOUND:
return SX_ENTRY_NOT_FOUND
sx_check_rc(rc)
entry = sx_uc_route_get_entry_t_arr_getitem(entries_array, 0)
if entry.route_data.type == SX_UC_ROUTE_TYPE_NEXT_HOP:
return entry.route_data.uc_route_param.ecmp_id
return SX_ENTRY_NOT_FOUND
finally:
delete_sx_uc_route_get_entry_t_arr(entries_array)
delete_uint32_t_p(entries_cnt_p)
delete_sx_ip_prefix_t_p(ip_prefix_p)
def sx_router_ecmp_nexthops_get(handle, ecmp_id):
""" Get next hops for a given ECMP id
Args:
handle (sx_api_handle_t): SDK handle
ecmp_id (int): ECMP id
Returns:
list: List of next hops
"""
try:
next_hops = []
next_hop_count_p = new_uint32_t_p()
uint32_t_p_assign(next_hop_count_p, 0)
rc = sx_api_router_operational_ecmp_get(handle, ecmp_id, None, next_hop_count_p)
sx_check_rc(rc)
next_hop_count = uint32_t_p_value(next_hop_count_p)
next_hop_list_p = new_sx_next_hop_t_arr(next_hop_count)
rc = sx_api_router_operational_ecmp_get(handle, ecmp_id, next_hop_list_p, next_hop_count_p)
sx_check_rc(rc)
next_hop_count = uint32_t_p_value(next_hop_count_p)
for i in range(next_hop_count):
next_hop = sx_next_hop_t_arr_getitem(next_hop_list_p, i)
if next_hop.next_hop_key.type == SX_NEXT_HOP_TYPE_IP:
next_hops.append(next_hop)
return next_hops
finally:
delete_sx_next_hop_t_arr(next_hop_list_p)
delete_uint32_t_p(next_hop_count_p)
def sx_get_router_interface(handle, vrid, rif):
""" Get router interface information
Args:
handle (sx_api_handle_t): SDK handle
vrid (sx_router_id_t): virtual router id
rif (sx_router_interface_t): router interface id
Returns:
dict : Dictionary contains interface parameters
"""
try:
vrid_p = new_sx_router_id_t_p()
sx_router_id_t_p_assign(vrid_p, vrid)
ifc_p = new_sx_router_interface_param_t_p()
ifc_attr_p = new_sx_interface_attributes_t_p()
rif_params = {}
rc = sx_api_router_interface_get(handle, rif, vrid_p, ifc_p, ifc_attr_p)
sx_check_rc(rc)
if ifc_p.type == SX_L2_INTERFACE_TYPE_PORT_VLAN:
rif_params[PORT] = ifc_p.ifc.port_vlan.port
if ifc_p.type == SX_L2_INTERFACE_TYPE_VPORT:
rif_params[VPORT] = ifc_p.ifc.vport.vport
if ifc_p.type == SX_L2_INTERFACE_TYPE_VLAN:
rif_params[VLAN] = ifc_p.ifc.vlan.vlan
return rif_params
finally:
delete_sx_interface_attributes_t_p(ifc_attr_p)
delete_sx_router_interface_param_t_p(ifc_p)
delete_sx_router_id_t_p(vrid_p)
def sx_port_vport_base_get(handle, vport):
""" Get SDK logical index and vlan for given vport
Args:
handle (sx_api_handle_t): SDK handle
vport (sx_port_id_t): SDK vport id
Returns:
sx_port_log_id_t : SDK logical index
"""
try:
vlan_id_p = new_sx_vlan_id_t_p()
logical_port_p = new_sx_port_log_id_t_p()
rc = sx_api_port_vport_base_get(handle, vport, vlan_id_p, logical_port_p)
sx_check_rc(rc)
logical_port = sx_port_log_id_t_p_value(logical_port_p)
vlan_id = sx_vlan_id_t_p_value(vlan_id_p)
return logical_port, vlan_id
finally:
delete_sx_port_log_id_t_p(logical_port_p)
delete_sx_vlan_id_t_p(vlan_id_p)
def sx_router_neigh_get_mac(handle, rif, addr):
""" Get neighbour mac address
Args:
handle (sx_api_handle_t): SDK handle
rif (sx_port_id_t): SDK vport id
addr (sx_ip_addr_t): Neighbour IP address
Returns:
str : Neighbour mac address
"""
try:
neigh_entry_cnt_p = new_uint32_t_p()
neigh_entry_list_p = new_sx_neigh_get_entry_t_arr(1)
filter_p = new_sx_neigh_filter_t_p()
neigh_filter = sx_neigh_filter_t()
neigh_filter.filter_by_rif = SX_KEY_FILTER_FIELD_NOT_VALID
neigh_filter.rif = 0
sx_neigh_filter_t_p_assign(filter_p, neigh_filter)
rc = sx_api_router_neigh_get(handle, SX_ACCESS_CMD_GET, rif, addr, filter_p, neigh_entry_list_p, neigh_entry_cnt_p)
if rc == SX_STATUS_ENTRY_NOT_FOUND:
return None
sx_check_rc(rc)
neighbor_entry = sx_neigh_get_entry_t_arr_getitem(neigh_entry_list_p, 0)
return neighbor_entry.neigh_data.mac_addr.to_str()
finally:
delete_sx_neigh_filter_t_p(filter_p)
delete_sx_neigh_get_entry_t_arr(neigh_entry_list_p)
delete_uint32_t_p(neigh_entry_cnt_p)
def sx_fdb_uc_mac_addr_get(handle, vlan_id, mac_addr):
""" Get UC mac entry from FDB
Args:
handle (sx_api_handle_t): SDK handle
vlan_id (sx_vlan_id_t): VLAN id
mac_addr (str): mac address
Returns:
sx_fdb_uc_mac_addr_params_t : FDB mac entry
"""
try:
key = sx_fdb_uc_mac_addr_params_t()
key.fid_vid = vlan_id
key.mac_addr = ether_addr(mac_addr)
key.action = SX_FDB_ACTION_FORWARD
key_p = copy_sx_fdb_uc_mac_addr_params_t_p(key)
key_filter = sx_fdb_uc_key_filter_t()
key_filter.filter_by_fid = SX_FDB_KEY_FILTER_FIELD_VALID
key_filter.filter_by_mac_addr = SX_FDB_KEY_FILTER_FIELD_VALID
key_filter.filter_by_log_port = SX_FDB_KEY_FILTER_FIELD_NOT_VALID
key_filter.fid = vlan_id
key_filter.mac_addr = ether_addr(mac_addr)
key_filter_p = copy_sx_fdb_uc_key_filter_t_p(key_filter)
data_cnt_p = copy_uint32_t_p(SX_FDB_MAX_GET_ENTRIES)
mac_list_p = new_sx_fdb_uc_mac_addr_params_t_arr(SX_FDB_MAX_GET_ENTRIES)
rc = sx_api_fdb_uc_mac_addr_get(handle, 0, SX_ACCESS_CMD_GET_FIRST, SX_FDB_UC_ALL, key_p, key_filter_p, mac_list_p, data_cnt_p)
if rc == SX_STATUS_ENTRY_NOT_FOUND:
return None
sx_check_rc(rc)
data_cnt = uint32_t_p_value(data_cnt_p)
if data_cnt == 0:
return None
assert data_cnt == 1, "Got unexpected macs amount, mac {} vlan {} data_cnt {}".format(mac_addr, vlan_id, data_cnt)
mac_entry = sx_fdb_uc_mac_addr_params_t_arr_getitem(mac_list_p, 0)
assert mac_entry.dest_type == SX_FDB_UC_MAC_ADDR_DEST_TYPE_LOGICAL_PORT, "Got unexpected mac entry type {}".format(mac_entry.dest_type)
return mac_entry
finally:
delete_sx_fdb_uc_mac_addr_params_t_arr(mac_list_p)
delete_uint32_t_p(data_cnt_p)
delete_sx_fdb_uc_key_filter_t_p(key_filter_p)
delete_sx_fdb_uc_mac_addr_params_t_p(key_p)
def sx_lag_port_group_get(handle, lag_id):
""" Get LAG members
Args:
handle (sx_api_handle_t): SDK handle
lag_id (sx_port_log_id_t): LAG id
Returns:
list : list of LAG members logical indices
"""
try:
lag_members = []
port_count_p = new_uint32_t_p()
uint32_t_p_assign(port_count_p, 0)
port_arr = None
rc = sx_api_lag_port_group_get(handle, 0, lag_id, port_arr, port_count_p)
sx_check_rc(rc)
port_count = uint32_t_p_value(port_count_p)
if port_count > 0:
port_arr = new_sx_port_log_id_t_arr(port_count)
rc = sx_api_lag_port_group_get(handle, 0, lag_id, port_arr, port_count_p)
sx_check_rc(rc)
for i in range(port_count):
lag_members.append(sx_port_log_id_t_arr_getitem(port_arr, i))
return lag_members
finally:
delete_sx_port_log_id_t_arr(port_arr)
delete_uint32_t_p(port_count_p)
def sx_vlan_ports_get(handle, vlan_id):
""" Get VLAN member ports
Args:
handle (sx_api_handle_t): SDK handle
vlan_id (sx_vid_t): VLAN id
Returns:
list : list of VLAN members logical indexes
"""
try:
vlan_members = []
port_cnt_p = new_uint32_t_p()
uint32_t_p_assign(port_cnt_p, 0)
rc = sx_api_vlan_ports_get(handle, SWITCH_ID, vlan_id, None, port_cnt_p)
sx_check_rc(rc)
port_cnt = uint32_t_p_value(port_cnt_p)
vlan_port_list_p = new_sx_vlan_ports_t_arr(port_cnt)
rc = sx_api_vlan_ports_get(handle, SWITCH_ID, vlan_id, vlan_port_list_p, port_cnt_p)
sx_check_rc(rc)
for i in range(0, port_cnt):
vlan_port = sx_vlan_ports_t_arr_getitem(vlan_port_list_p, i)
vlan_members.append(vlan_port.log_port)
return vlan_members
finally:
delete_sx_vlan_ports_t_arr(vlan_port_list_p)
delete_uint32_t_p(port_cnt_p)
def sx_make_ip_prefix_v4(addr, mask):
""" Create IPv4 prefix
Args:
addr (str): IPv4 address
mask (str): Network mask
Returns:
sx_ip_prefix_t : IPv4 prefix
"""
ip_prefix = sx_ip_prefix_t()
ip_prefix.version = SX_IP_VERSION_IPV4
ip_prefix.prefix.ipv4.addr.s_addr = struct.unpack('>I', socket.inet_pton(socket.AF_INET, addr))[0]
ip_prefix.prefix.ipv4.mask.s_addr = struct.unpack('>I', socket.inet_pton(socket.AF_INET, mask))[0]
return ip_prefix
def sx_make_ip_prefix_v6(addr, mask):
""" Create IPv6 prefix
Args:
addr (str): IPv6 address
mask (str): Network mask
Returns:
sx_ip_v6_prefix_t : IPv6 prefix
"""
addr = ipv6_str_to_bytes(str(addr))
mask = ipv6_str_to_bytes(str(mask))
ip_prefix = sx_ip_prefix_t()
ip_prefix.version = SX_IP_VERSION_IPV6
for i in range(0, 16):
uint8_t_arr_setitem(ip_prefix.prefix.ipv6.addr._in6_addr__in6_u._in6_addr___in6_u__u6_addr8, i, addr[i])
uint8_t_arr_setitem(ip_prefix.prefix.ipv6.mask._in6_addr__in6_u._in6_addr___in6_u__u6_addr8, i, mask[i])
return ip_prefix
def ipv6_str_to_bytes(address):
# Load the ipv6 string into 4 dwords
dwords = struct.unpack("!IIII", socket.inet_pton(socket.AF_INET6, address))
# Convert dwords endian using ntohl
total_bytes = [socket.ntohl(i) for i in dwords]
# Finally, convert back to bytes
out = struct.pack(">IIII", total_bytes[0], total_bytes[1], total_bytes[2], total_bytes[3])
return [i for i in out]
def sx_ip_addr_to_str(ip_addr):
if ip_addr.version == SX_IP_VERSION_IPV4:
return socket.inet_ntop(socket.AF_INET, struct.pack('!I', ip_addr.addr.ipv4.s_addr))
else:
bytes = ip_addr.addr.ipv6._in6_addr__in6_u._in6_addr___in6_u__u6_addr8
byte_arr = []
for i in range(0, 16):
byte_arr.append(uint8_t_arr_getitem(bytes, i))
if sys.byteorder == "little":
dwords = struct.pack("!BBBBBBBBBBBBBBBB", byte_arr[3], byte_arr[2], byte_arr[1], byte_arr[0], byte_arr[7],
byte_arr[6], byte_arr[5], byte_arr[4], byte_arr[11], byte_arr[10], byte_arr[9],
byte_arr[8], byte_arr[15], byte_arr[14], byte_arr[13], byte_arr[12])
else:
dwords = struct.pack("!BBBBBBBBBBBBBBBB", byte_arr[0], byte_arr[1], byte_arr[2], byte_arr[3], byte_arr[4],
byte_arr[5], byte_arr[6], byte_arr[7], byte_arr[8], byte_arr[9], byte_arr[10],
byte_arr[11], byte_arr[12], byte_arr[13], byte_arr[14], byte_arr[15])
return socket.inet_ntop(socket.AF_INET6, dwords)

View File

@ -0,0 +1,140 @@
#!/usr/bin/env python3
PACKET_SCHEME = {
"type" : "object",
"properties" :
{
"packet_info" :
{
"type" : "object",
"properties" :
{
"outer" :
{
"type" : "object",
"properties" :
{
"layer2" :
{
"type" : "object",
"properties" :
{
"smac": {"type": "string"},
"dmac": {"type": "string"},
"ethertype": {"type": "number"},
"outer_vid": {"type": "number"},
"outer_pcp": {"type": "number"},
"outer_dei": {"type": "number"},
"inner_vid": {"type": "number"},
"inner_pcp": {"type": "number"},
"inner_dei": {"type": "number"}
}
},
"arp" :
{
"type" : "object",
"properties" :
{
"spa": {"type": "string"},
"tpa": {"type": "string"}
}
},
"ipv4" :
{
"type" : "object",
"properties" :
{
"sip": {"type": "string"},
"dip": {"type": "string"},
"proto": {"type": "number"}
},
"required": ["dip"]
},
"ipv6" :
{
"type" : "object",
"properties" :
{
"sip": {"type": "string"},
"dip": {"type": "string"},
"mflag": {"type": "number"},
"next_header": {"type": "number"},
"dscp": {"type": "number"},
"ecn": {"type": "number"},
"l3_length": {"type": "number"},
"flow_label": {"type": "number"}
},
"required": ["dip"]
},
"tcp_udp" :
{
"type" : "object",
"properties" :
{
"sport": {"type": "number"},
"dport": {"type": "number"}
}
},
"vxlan_nvgre" :
{
"type" : "object",
"properties" :
{
"vni": {"type": "number"}
}
}
}
},
"inner" :
{
"type" : "object",
"properties" :
{
"layer2" :
{
"type" : "object",
"properties" :
{
"smac": {"type": "string"},
"dmac": {"type": "string"},
"ethertype": {"type": "number"}
}
},
"ipv4" :
{
"type" : "object",
"properties" :
{
"sip": {"type": "string"},
"dip": {"type": "string"},
"mflag": {"type": "number"},
"proto": {"type": "number"}
}
},
"ipv6" :
{
"type" : "object",
"properties" :
{
"sip": {"type": "string"},
"dip": {"type": "string"},
"mflag": {"type": "number"},
"next_header": {"type": "number"},
"flow_label": {"type": "number"}
}
},
"tcp_udp" :
{
"type" : "object",
"properties" :
{
"sport": {"type": "number"},
"dport": {"type": "number"}
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,122 @@
#!/usr/bin/env python3
from python_sdk_api.sx_api import *
import inspect
DEVICE_ID = 1
SWITCH_ID = 0
ETHERNET_PREFIX = 'Ethernet'
ASIC_MAX_LANES = {SX_CHIP_TYPE_SPECTRUM: 4, SX_CHIP_TYPE_SPECTRUM2: 4,
SX_CHIP_TYPE_SPECTRUM3: 8, SX_CHIP_TYPE_SPECTRUM4: 8}
def sx_get_ports_map(handle):
""" Get ports map from SDK logical index to SONiC index
Args:
handle (sx_api_handle_t): SDK handle
Returns:
dict : Dictionary of ports indices. Key is SDK logical index, value is SONiC index (4 for Ethernet4)
"""
try:
ports_map = {}
# Get chip type
chip_type = sx_get_chip_type(handle)
# Get ports count
port_cnt_p = new_uint32_t_p()
rc = sx_api_port_device_get(handle, DEVICE_ID, SWITCH_ID, None, port_cnt_p)
sx_check_rc(rc)
# Get ports
port_cnt = uint32_t_p_value(port_cnt_p)
port_attributes_list = new_sx_port_attributes_t_arr(port_cnt)
rc = sx_api_port_device_get(handle, DEVICE_ID, SWITCH_ID, port_attributes_list, port_cnt_p)
sx_check_rc(rc)
for i in range(0, port_cnt):
port_attributes = sx_port_attributes_t_arr_getitem(port_attributes_list, i)
label_port = port_attributes.port_mapping.module_port
logical_port = port_attributes.log_port;
lane_bmap = port_attributes.port_mapping.lane_bmap;
if (is_phy_port(port_attributes.log_port) == False):
continue
# Calculate sonic index (sonic index=4 for Ethernet4)
lane_index = get_lane_index(lane_bmap, ASIC_MAX_LANES[chip_type])
assert lane_index != -1, "Failed to calculate port index"
sonic_index = label_port * ASIC_MAX_LANES[chip_type] + lane_index;
sonic_interface = ETHERNET_PREFIX + str(sonic_index)
ports_map[logical_port] = sonic_interface
return ports_map
finally:
delete_sx_port_attributes_t_arr(port_attributes_list)
delete_uint32_t_p(port_cnt_p)
def sx_get_chip_type(handle):
""" Get system ASIC type
Args:
handle (sx_api_handle_t): SDK handle
Returns:
sx_chip_types_t : Chip type
"""
try:
device_info_cnt_p = new_uint32_t_p()
uint32_t_p_assign(device_info_cnt_p, 1)
device_info_cnt = uint32_t_p_value(device_info_cnt_p)
device_info_list_p = new_sx_device_info_t_arr(device_info_cnt)
rc = sx_api_port_device_list_get(handle, device_info_list_p, device_info_cnt_p)
sx_check_rc(rc)
device_info = sx_device_info_t_arr_getitem(device_info_list_p, SWITCH_ID)
chip_type = device_info.dev_type
if chip_type == SX_CHIP_TYPE_SPECTRUM_A1:
chip_type = SX_CHIP_TYPE_SPECTRUM
return chip_type
finally:
delete_sx_device_info_t_arr(device_info_list_p)
delete_uint32_t_p(device_info_cnt_p)
def get_lane_index(lane_bmap, max_lanes):
""" Get index of first lane in use (2 for 00001100)
Args:
lane_bmap (int): bitmap indicating module lanes in use
max_lanes (int): Max lanes in module
Returns:
int : index of the first bit set to 1 in lane_bmap
"""
for lane_idx in range(0, max_lanes):
if (lane_bmap & 0x1 == 1):
return lane_idx
lane_bmap = lane_bmap >> 1
def sx_check_rc(rc):
if rc is not SX_STATUS_SUCCESS:
# Get the calling function name from the last frame
cf = inspect.currentframe().f_back
func_name = inspect.getframeinfo(cf).function
error_info = func_name + ' failed with rc = ' + str(rc)
raise Exception(error_info)
def get_port_type(log_port_id):
return (log_port_id & SX_PORT_TYPE_ID_MASK) >> SX_PORT_TYPE_ID_OFFS
def is_phy_port(log_port_id):
return get_port_type(log_port_id) == SX_PORT_TYPE_NETWORK
def is_lag(log_port_id):
return get_port_type(log_port_id) == SX_PORT_TYPE_LAG