DOCA SDK Documentation

P4 Runtime Controller

DPL applications are deployed to the NVIDIA® BlueField® networking platform (DPU or SuperNIC) via the P4Runtime API. Since DPL is derived from the P4-16 language, it is compatible with the P4Runtime specification, enabling standard runtime interaction with the compiled DPL pipeline

Introduction

The P4 Runtime shell (p4runtime_sh) is a Python-based CLI tool used to interact with this API. It is primarily used for loading DPL programs, testing match-action tables, and debugging pipeline behavior.

  • Loading simple DPL programs

  • Testing match-action tables

  • Debugging pipeline behavior

The shell can be invoked using the launch script provided in the DPL Development container.

For detailed instructions, refer to "Loading DPL Applications" documentation.

Basic Information and Inspection

These commands allow you to view the high-level structure of your loaded P4 program.

  • Retrieves the content of p4info.txt for the currently loaded DPL program:

    Python
    p4info
    
  • Lists all available tables:

    tables

  • Displays information about a specific table:

    tables["<P4_TABLE_NAME>"]

  • Lists all defined actions:

    actions

  • Shows parameters and sizes for a specific action:

    actions["<P4_ACTION_NAME>"]

Working with P4 Table Entries

Specifying Match Keys

When creating or modifying entries, you must define the match keys. The syntax depends on the match type defined in your DPL program.

Match Method

Syntax for Specifying Match Key Value

Note

Exact

te.match["<KEY_NAME>"] = "<VALUE>"

Exact value match.

Ternary

te.match["<KEY_NAME>"] = "<VALUE>&&&<MASK>"

te.priority = <PRIORITY_VALUE>

The mask is provided in the match line, separated by &&&. If the mask is not specified, a full match mask will be used.

LPM

te.match["<KEY_NAME>"] = "<VALUE>/<PREFIX_LEN>"

The prefix length is provided in the match line, separated by /. If the prefix length is not specified, the full field bitwidth is used.

Range

te.match["<KEY_NAME>"] = "<MIN_VALUE>..<MAX_VALUE>"

The values are are separated by .. , both values must be present.

Adding Entries

To add a standard entry, you must define the table, the action, the match keys, and the action parameters.

  • Regular entry:

    te = table_entry["<P4_TABLE_NAME>"](action="<P4_ACTION_NAME>")
    te.match["<MATCH_KEY_NAME>"] = "<MATCH_VALUE>"
    te.action["<PARAMETER_NAME>"] = "<PARAMETER_VALUE>"
    te.insert()

  • Entry with idle timeout:

    te = table_entry["<P4_TABLE_NAME>"](action="<P4_ACTION_NAME>")
    te.match["<MATCH_KEY_NAME>"] = "<MATCH_VALUE>"
    te.action["<PARAMETER_NAME>"] = "<PARAMETER_VALUE>"
    te.idle_timeout_ns = 3000000000 # Example: 3 seconds
    te.insert()

Reading Entries

  • Read a specific entry:

    te = table_entry["<P4_TABLE_NAME>"]
    te.match["<MATCH_KEY_NAME>"] = "<MATCH_VALUE>"
    te.read(lambda te: print(te))

  • Read all entries in a table:

    te = table_entry["<P4_TABLE_NAME>"]
    te.is_default = False
    te.read(lambda te: print(te))

  • Read time since last hit:

    Only for tables with idle-timeout enabled.

    te = table_entry["<P4_TABLE_NAME>"]
    te.time_since_last_hit.elapsed_ns = 0 # Enable reading timestamp
    te.match["<MATCH_KEY_NAME>"] = "<MATCH_VALUE>"
    te.read(lambda te: print(te))

Deleting Entries

  • Delete a specific entry:

    te = table_entry["<P4_TABLE_NAME>"]
    te.match["<MATCH_KEY_NAME>"] = "<MATCH_VALUE>"
    te.delete()

  • Delete all entries in a table:

    te = table_entry["<P4_TABLE_NAME>"]
    te.read(lambda te: te.delete())

Modifying Entries

You can modify specific action parameters of an existing table entry without removing it, provided those parameters are explicitly marked as modifiable in the P4 program. 

Modification is supported only for action parameters annotated with @nv_modifiable. Refer to DOCA Target Architecture for more details.

Match keys, Action IDs, and non-annotated parameters cannot be changed in place. To modify these fields, you must delete the existing entry and insert a new one.

The following example demonstrates how to update a modifiable parameter:

Python
# Select the table and action
te = table_entry["<P4_TABLE_NAME>"](action="<P4_ACTION_NAME>")

# Define the match keys to identify the specific entry
te.match["<MATCH_KEY_NAME>"] = "<MATCH_VALUE>"

# Update the specific parameter (Must be annotated with @nv_modifiable)
te.action["<PARAMETER_NAME>"] = "<NEW_VALUE>"

# Commit the modification
te.modify()

Managing Default Entries

Default entries are implicit. They cannot be inserted or deleted, only modified or read.

  • Reading a default entry:

    te = table_entry["<P4_TABLE_NAME>"]
    te.is_default = True
    te.read(lambda te: print(te))

  • Reading with counter: 

    te = table_entry["<P4_TABLE_NAME>"]
    te.is_default = True
    te.counter_data.byte_count = 0  # Request counter data
    te.read(lambda te: print(te))

  • Modifying a default entry: Modification follows the same rules as standard entries. You can only update action parameters annotated with @nv_modifiable.

    te = table_entry["<P4_TABLE_NAME>"](action="<P4_ACTION_NAME>")
    te.is_default = True
    te.action["<@nv_modifiable PARAMETER_NAME>"] = "<PARAMETER_VALUE>"
    te.modify()

Working with Counters

Direct Counters

Direct counters are attached to specific table entries.

  • List counters: direct_counters

  • Read specific entry counter:

    Python
    ce = DirectCounterEntry("<P4_DIRECT_COUNTER_NAME>")
    ce.table_entry.match["<KEY_NAME>"] = "<VALUE>"
    ce.read(lambda ce: print(ce))
    
  • Read default entry counter: Set ce.table_entry.is_default = True before reading.

  • Clear specific counter: Set ce.packet_count = 0 and call ce.modify().

  • Clear all counters:

    Python
    for dc in direct_counters:
        ce = DirectCounterEntry(dc.name)
        ce.packet_count = 0
        ce.modify()
    

Indirect Counters

Indirect counters are accessed by index.

  • List counters: counters

  • Read specific index:

    Python
    ce = counter_entry["<P4_INDIRECT_COUNTER_NAME>"]
    ce.index = <INDEX>
    ce.read(lambda ce: print(ce))
    
  • Clear specific index: Set ce.byte_count = 0 and ce.packet_count = 0, then call ce.modify().

Working with Meters

Direct Meters

  • List meters: direct_meters

  • Read specific meter:

    Python
    me = DirectMeterEntry("<P4_DIRECT_METER_NAME>")
    me.table_entry.match["<KEY>"] = "<VALUE>"
    me.read(lambda me: print(me))
    
  • Configure meter: Set config values (cir, cburst, pir, pburst) and call me.modify().

Indirect Meters

  • List meters: meters

  • Read specific index:

    Python
    me = meter_entry["<P4_INDIRECT_METER_NAME>"]
    me.index = <INDEX>
    me.read(lambda me: print(me))
    
  • Clear meter: Set me.cir = 0 (and other rates) and call me.modify().

Packet I/O and Notifications

Packet In (Sniffing)

To capture packets sent from the DPL Runtime to the Controller:

Python
packet_in.sniff(lambda m: print(m), timeout=10)

For detailed parsing, you can import impacket and decode the payload

Packet Out (Sending)

To send packets from the Controller to the DPL Runtime. You typically use scapy to build the packet first.

Python
# 1. Build packet string (using Scapy or raw bytes)
my_pkt = b'<PACKET_BYTE_STRING>' 

# 2. Send packet with metadata
# Repeat metadata args for every metadata defined in P4
packet_out(payload=my_pkt, <METADATA_NAME>="<VALUE>").send()

Idle Timeout Notifications

To receive notifications for entry timeouts (requires nv_support_timeout in DPL source):

Python
idle_timeout_notification.sniff(lambda m: print(m), timeout=10)

Last updated: