Skip to content

Commit

Permalink
[dash] Common package dash-pipeline-utils (#667)
Browse files Browse the repository at this point in the history
Add package dash-pipeline-utils.

This package includes utils to configure DASH pipeline (bmv2) via p4runtime directly. It is a supplementary of DASH SAI, which is not doable/proper to do bmv2 specific configurations.
  • Loading branch information
jimmyzhai authored Feb 25, 2025
1 parent 83f57f0 commit 8012472
Show file tree
Hide file tree
Showing 6 changed files with 391 additions and 221 deletions.
6 changes: 6 additions & 0 deletions dash-pipeline/dockerfiles/Dockerfile.saithrift-client
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ RUN cd /saithrift-0.9 && \
cd /SAI/test/ptf && \
sudo python3 setup.py install

# Copy and install dash pipeline utils
ADD dash-pipeline/utils/ /dash-pipeline-utils
RUN cd /dash-pipeline-utils && \
sudo python3 setup.py bdist_wheel && \
sudo pip install dist/dash_pipeline_utils*.whl

# Copy distro test-cases into container, making it standalone (doesn't need host FS contents).
# For dev, host directories can be mounted to another container directory to "see" tests in development
ADD test/test-cases/ tests/
Expand Down
30 changes: 30 additions & 0 deletions dash-pipeline/utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# DASH pipeline utils

## Overview

This package includes utils to configure DASH pipeline (bmv2) via p4runtime
directly. It is a supplementary of DASH SAI, which is not doable/proper to do
bmv2 specific configurations.

## Usage

### internal configuration of bmv2 pipeline

```python
neighbor_mac = "xx:xx:xx:xx:xx:xx"
dut_mac = "xx:xx:xx:xx:xx:xx"
internal_config = P4InternalConfigTable(target = "localhost:9559")
internal_config.set(neighbor_mac = neighbor_mac, mac = dut_mac)
internal_config.set(flow_enabled = 1)
internal_config.get()
internal_config.unset()
```

### underlay routing

```python
underlay_routing = P4UnderlayRoutingTable(target = "localhost:9559")
underlay_routing.set(ip_prefix = '::10.0.1.0', ip_prefix_len = 120, next_hop_id = 1)
underlay_routing.get(ip_prefix = '::10.0.1.0', ip_prefix_len = 120))
underlay_routing.unset(ip_prefix = '::10.0.1.0', ip_prefix_len = 120))
```
274 changes: 274 additions & 0 deletions dash-pipeline/utils/dash_pipeline_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import grpc
from p4.v1 import p4runtime_pb2
from p4.v1 import p4runtime_pb2_grpc
import socket


def mac_in_bytes(mac):
return bytes(int(b, 16) for b in mac.split(":"))


class P4info():
def __init__(self, stub):
self.config = P4info.get_pipeline_config(stub)

@staticmethod
def get_pipeline_config(stub):
try:
req = p4runtime_pb2.GetForwardingPipelineConfigRequest()
req.device_id = 0
req.response_type = p4runtime_pb2.GetForwardingPipelineConfigRequest.ResponseType.P4INFO_AND_COOKIE
return stub.GetForwardingPipelineConfig(req).config.p4info
except Exception as e:
print(f'gRPC error: str({e})')
return None

def get_table(self, name):
for table in self.config.tables:
if table.preamble.name == name:
return table

return None

def get_action(self, name):
for action in self.config.actions:
if action.preamble.name == name:
return action

return None


class P4Table():
def __init__(self, target=None):
if not target:
target='localhost:9559'
channel = grpc.insecure_channel(target)
self.stub = p4runtime_pb2_grpc.P4RuntimeStub(channel)
self.p4info = P4info(self.stub)

def read(self, table_id, match_list = None, priority = None):
entry = p4runtime_pb2.TableEntry()
entry.table_id = table_id
if match_list:
entry.match.extend(match_list)
if priority != None:
entry.priority = priority

req = p4runtime_pb2.ReadRequest()
req.device_id = 0
entity = req.entities.add()
entity.table_entry.CopyFrom(entry)
for response in self.stub.Read(req):
for entity in response.entities:
yield entity.table_entry

def write(self, entry, update_type):
req = p4runtime_pb2.WriteRequest()
req.device_id = 0
update = req.updates.add()
update.type = update_type
update.entity.table_entry.CopyFrom(entry)
self.stub.Write(req)

def find(self, table_id, user_match_list, priority = None):
for entry in self.read(table_id, user_match_list, priority):
return entry
return None

def update(self, table_id, user_match_list, action_id, user_params, priority = None):
entry = self.find(table_id, user_match_list, priority)
if entry:
changed = 0

for param in entry.action.action.params:
if param.param_id in user_params:
byte_data, _ = user_params[param.param_id]
if byte_data is not None and byte_data != param.value:
param.value = byte_data
changed += 1

if changed:
self.write(entry, p4runtime_pb2.Update.MODIFY)
return

# Add one entry
entry = p4runtime_pb2.TableEntry()
entry.table_id = table_id
if priority is not None:
entry.priority = priority
entry.match.extend(user_match_list)

entry.action.action.action_id = action_id
action = entry.action.action

for param_id,param_value in user_params.items():
param = action.params.add()
param.param_id = param_id
if param_value[0] is not None:
param.value = param_value[0]
else:
param.value = param_value[1]

self.write(entry, p4runtime_pb2.Update.INSERT)


class P4InternalConfigTable(P4Table):
def __init__(self, target=None):
super(P4InternalConfigTable, self).__init__(target)
self.p4info_table = self.p4info.get_table("dash_ingress.dash_lookup_stage.pre_pipeline_stage.internal_config")
self.match_id_map = { mf.name: mf.id for mf in self.p4info_table.match_fields}
self.set_internal_config = self.p4info.get_action("dash_ingress.dash_lookup_stage.pre_pipeline_stage.set_internal_config")
self.set_internal_config_id_map = { param.name: param.id for param in self.set_internal_config.params }

def to_match_list(self, appliance_id :int = 0):
match = p4runtime_pb2.FieldMatch()
match.field_id = self.match_id_map['meta.appliance_id']
match.ternary.value = appliance_id.to_bytes(1, byteorder='big')
match.ternary.mask = b'\xff'

return [match]

def get(self, appliance_id :int = 0):
'''
Get dash pipeline internal config
'''

user_match_list = self.to_match_list(appliance_id)
return self.find(self.p4info_table.preamble.id, user_match_list, priority = 1)

def set(self,
appliance_id :int = 0,
neighbor_mac :str = None,
mac :str = None,
cpu_mac :str = None,
flow_enabled :int = None):
'''
Set dash pipeline internal config by updating table entry of internal_config.
if one argument is not specifed, the action param is not changed in the
existing table entry, otherwise set default value in new table entry.
'''

if neighbor_mac is not None:
neighbor_mac = mac_in_bytes(neighbor_mac)
if mac is not None:
mac = mac_in_bytes(mac)
if cpu_mac is not None:
cpu_mac = mac_in_bytes(cpu_mac)
if flow_enabled is not None:
flow_enabled = flow_enabled.to_bytes(1, byteorder='big')

user_match_list = self.to_match_list(appliance_id)

# param_id -> (value, default value)
user_params = {
self.set_internal_config_id_map['neighbor_mac']: (
neighbor_mac, b'\x00\x00\x00\x00\x00\x00'
),
self.set_internal_config_id_map['mac']: (
mac, b'\x00\x00\x00\x00\x00\x00'
),
self.set_internal_config_id_map['cpu_mac']: (
cpu_mac, b'\x00\x00\x00\x00\x00\x00'
),
self.set_internal_config_id_map['flow_enabled']: (
flow_enabled, b'\x00'
)
}

self.update(self.p4info_table.preamble.id,
user_match_list,
self.set_internal_config.preamble.id,
user_params,
priority = 1)

def unset(self, appliance_id :int = 0):
'''
Unset dash pipeline internal config
'''

entry = self.get(appliance_id)
if entry:
self.write(entry, p4runtime_pb2.Update.DELETE)
else:
print(f'Internal config for appliance {appliance_id} not found.')


class P4UnderlayRoutingTable(P4Table):
def __init__(self, target=None):
super(P4UnderlayRoutingTable, self).__init__(target)
self.p4info_table = self.p4info.get_table("dash_ingress.underlay.underlay_routing")
self.match_id_map = { mf.name: mf.id for mf in self.p4info_table.match_fields}
self.pkt_act = self.p4info.get_action("dash_ingress.underlay.pkt_act")
self.pkt_act_id_map = { param.name: param.id for param in self.pkt_act.params }

def to_match_list(self,
ip_prefix :str = '::', # ipv6 string, ::x.x.x.x for ipv4
ip_prefix_len :int = 128): # in bits
match = p4runtime_pb2.FieldMatch()
match.field_id = self.match_id_map['meta.dst_ip_addr']
match.lpm.value = socket.inet_pton(socket.AF_INET6, ip_prefix)
match.lpm.prefix_len = ip_prefix_len

return [match]

def get(self,
ip_prefix :str = '::', # ipv6 string, ::x.x.x.x for ipv4
ip_prefix_len :int = 128): # in bits
'''
Get underlay route entry with ip prefix
'''

user_match_list = self.to_match_list(ip_prefix, ip_prefix_len)
return self.find(self.p4info_table.preamble.id, user_match_list)

def set(self,
ip_prefix :str = '::', # ipv6 string, ::x.x.x.x for ipv4
ip_prefix_len :int = 128, # in bits
packet_action :int = None,
next_hop_id :int = None):
'''
Set underlay route entry with ip prefix
if one argument is not specifed, the action param is not changed in the
existing table entry, otherwise set default value in new table entry.
'''

if packet_action is not None:
packet_action = packet_action.to_bytes(2, byteorder='big')
if next_hop_id is not None:
next_hop_id = next_hop_id.to_bytes(2, byteorder='big')

user_match_list = self.to_match_list(ip_prefix, ip_prefix_len)

# param_id -> (value, default value)
user_params = {
self.pkt_act_id_map['packet_action']: (
packet_action, b'\x00\x01' # ACTION_FORWARD
),
self.pkt_act_id_map['next_hop_id']: (
next_hop_id, b'\x00\x00' # port 0
)
}

self.update(self.p4info_table.preamble.id,
user_match_list,
self.pkt_act.preamble.id,
user_params)

def unset(self,
ip_prefix :str = '::', # ipv6 string, ::x.x.x.x for ipv4
ip_prefix_len :int = 128): # in bits
'''
Unset underlay route entry with ip prefix
'''

entry = self.get(ip_prefix, ip_prefix_len)
if entry:
self.write(entry, p4runtime_pb2.Update.DELETE)
else:
print(f'Route entry for {ip_prefix}/{ip_prefix_len} not found.')
20 changes: 20 additions & 0 deletions dash-pipeline/utils/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from setuptools import setup, find_packages

setup(
name='dash-pipeline-utils',
version='1.0',
packages=find_packages(),
py_modules = ['dash_pipeline_utils'],
install_requires = [
'protobuf>=3.20.1',
'p4runtime'
],
setup_requires = [
'wheel'
],
description='A Python package of dash pipeline utils',
license='Apache 2.0',
author='Junhua Zhai',
author_email='[email protected]',
url='https://github.com/sonic-net/DASH/tree/main/dash-pipeline/utils',
)
Loading

0 comments on commit 8012472

Please sign in to comment.