Finsy P4Runtime Controller Library
Finsy is a P4Runtime controller library written in Python using asyncio. Finsy includes support for gNMI.
Check out the examples directory
for some demonstration programs.
Installation
Finsy requires Python 3.10 or later. To install the latest version, type pip install finsy
.
P4Runtime Scripts
With Finsy, you can write a Python script that reads/writes P4Runtime entities for a single switch.
Here is a complete example that retrieves the P4Info from a switch:
import finsy as fy
async def main():
async with fy.Switch("sw1", "127.0.0.1:50001") as sw1:
print(sw1.p4info)
fy.run(main())
Here is another example that prints out all non-default table entries.
import finsy as fy
async def main():
async with fy.Switch("sw1", "127.0.0.1:50001") as sw1:
async for entry in sw1.read(fy.P4TableEntry()):
print(entry)
fy.run(main())
P4Runtime Controller
You can also write a P4Runtime controller that manages multiple switches independently. Your controller
can react to events from the Switch by changing the contents of P4 tables.
Each switch is managed by an async ready_handler
function. Your ready_handler
function can read or
update various P4Runtime entities in the switch. It can also create tasks to listen for
packets or digests.
When you write P4Runtime updates to the switch, you use a unary operator (+, -, ~) to specify the operation:
INSERT (+), DELETE (-) or MODIFY (~).
import finsy as fy
async def ready_handler(sw: fy.Switch):
await sw.delete_all()
await sw.write(
[
+fy.P4MulticastGroupEntry(1, replicas=[1, 2, 3, 255]),
~fy.P4TableEntry(
"ipv4",
action=fy.Action("flood"),
is_default_action=True,
),
]
)
async for packet in sw.read_packets():
print(f"{sw.name}: {packet}")
Use the SwitchOptions
class to specify each switch's settings, including the p4info/p4blob and
ready_handler
. Use the Controller
class to drive multiple switch connections. Each switch will call back
into your ready_handler
function after the P4Runtime connection is established.
from pathlib import Path
options = fy.SwitchOptions(
p4info=Path("hello.p4info.txt"),
p4blob=Path("hello.json"),
ready_handler=ready_handler,
)
controller = fy.Controller([
fy.Switch("sw1", "127.0.0.1:50001", options),
fy.Switch("sw2", "127.0.0.1:50002", options),
fy.Switch("sw3", "127.0.0.1:50003", options),
])
fy.run(controller.run())
Your ready_handler
can spawn concurrent tasks with the Switch.create_task
method. Tasks
created this way will have their lifetimes managed by the switch object.
If the switch disconnects or its role changes to backup, the task running your ready_handler
(and any tasks it spawned) will be cancelled and the ready_handler
will begin again.
For more examples, see the examples directory.
Switch Read/Write API
The Switch
class provides the API for interacting with P4Runtime switches. You will control
a Switch object with a "ready handler" function. The ready handler
is an
async function that is called when the switch is ready to accept commands.
Your ready handler
will typically write some control entities to the switch, then
listen for incoming events and react to them with more writes. You may occasionally read entities
from the switch.
When your ready handler
is invoked, there is already a P4Runtime channel established, with client
arbitration completed, and pipeline configured as specified in SwitchOptions
.
Here is an example skeleton program. The ready handler
is named ready()
.
async def ready(switch: fy.Switch):
if not switch.is_primary:
return
await switch.delete_all()
await switch.write(
)
async for packet in switch.read_packets():
await handle_packet(switch, packet)
The Switch class provides a switch.create_task
method to start a managed task.
Tasks allow you to perform concurrent operations on the same switch. We could have
written the last stanza above that reads packets in an infinite loop as a separate
task. It's okay for the ready handler function to return early; any tasks it
created will still run.
Writes
Use the write()
method to write one or more P4Runtime updates and packets.
A P4Runtime update supports one of three operations: INSERT, MODIFY or DELETE.
Some entities support all three operations. Other entities only support MODIFY.
Entity | Operations Permitted | Related Classes |
---|
P4TableEntry | INSERT, MODIFY, DELETE | Match , Action , IndirectAction , P4MeterConfig , P4CounterData , P4MeterCounterData |
P4ActionProfileMember | INSERT, MODIFY, DELETE | |
P4ActionProfileGroup | INSERT, MODIFY, DELETE | P4Member |
P4MulticastGroupEntry | INSERT, MODIFY, DELETE | |
P4CloneSessionEntry | INSERT, MODIFY, DELETE | |
P4DigestEntry | INSERT, MODIFY, DELETE | |
P4ExternEntry | INSERT, MODIFY, DELETE | |
P4RegisterEntry | MODIFY | |
P4CounterEntry | MODIFY | P4CounterData |
P4DirectCounterEntry | MODIFY | P4CounterData |
P4MeterEntry | MODIFY | P4MeterConfig , P4MeterCounterData |
P4DirectMeterEntry | MODIFY | |
P4ValueSetEntry | MODIFY | P4ValueSetMember |
Insert/Modify/Delete Updates
To specify the operation, use a unary +
(INSERT), ~
(MODIFY), or -
(DELETE). If you
do not specify the operation, write
will raise a ValueError
exception.
Here is an example showing how to insert and delete two different entities in the same WriteRequest.
await switch.write([
+fy.P4TableEntry(
"ipv4",
match=fy.Match(dest="192.168.1.0/24"),
action=fy.Action("forward", port=1),
),
-fy.P4TableEntry(
"ipv4",
match=fy.Match(dest="192.168.2.0/24"),
action=fy.Action("forward", port=2),
),
])
You should not insert, modify or delete the same entry in the same WriteRequest.
If you are performing the same operation on all entities, you can use the Switch
insert
, delete
, or modify
methods.
await switch.insert([
fy.P4MulticastGroupEntry(1, replicas=[1, 2, 3]),
fy.P4MulticastGroupEntry(2, replicas=[4, 5, 6]),
])
Modify-Only Updates
For entities that only support the modify operation, you do not need to specify the operation. (You can
optionally use ~
.)
await switch.write([
fy.P4RegisterEntry("reg1", index=0, data=0),
fy.P4RegisterEntry("reg1", index=1, data=1),
fy.P4RegisterEntry("reg1", index=2, data=2),
])
You can also use the modify
method:
await switch.modify([
fy.P4RegisterEntry("reg1", index=0, data=0),
fy.P4RegisterEntry("reg1", index=1, data=1),
fy.P4RegisterEntry("reg1", index=2, data=2),
])
If you pass a modify-only entity to the insert
or delete
methods, the P4Runtime server will
return an error.
Sending Packets
Use the write
method to send a packet.
await switch.write([fy.P4PacketOut(b"a payload.....", port=3)])
You can include other entities in the same call. Any non-update objects (e.g. P4PacketOut,
P4DigestListAck) will be sent before the WriteRequest.
Listening for Packets
To receive packets, use the async iterator Switch.read_packets()
.
In this example, pkt
is a P4PacketIn
object.
read_packets
can filter for a specific eth_type
.
async for pkt in switch.read_packets(eth_types={0x0806}):
print(pkt.payload)
print(pkt['ingress_port'])
Listening for Digests
To receive digests, use the async iterator Switch.read_digests
. You must specify
the name of the digest from your P4 program.
async for digest in switch.read_digests("digest_t"):
await switch.write([entry, ...])
await switch.write([digest.ack()])
To acknowledge the digest entry, you can write digest.ack()
.
Listening for Idle Timeouts
To receive idle timeout notifications, use the async iterator Switch.read_idle_timeouts
.
You will receive a P4IdleTimeoutNotification
which contains multiple table
entries -- one for each entry that timed out.
async for timeout in switch.read_idle_timeouts():
for entry in timeout.table_entry:
print(timeout.timestamp, entry)
Other Events
A P4 switch may report other events using the EventEmitter
API. See
the SwitchEvent
class for the event types. Each switch has a switch.ee
attribute that lets your code register for event callbacks.
Development and Testing
Perform these steps to set up your local environment for Finsy development, or try
the codespace. Finsy requires Python 3.10 or
later. If poetry is not installed, follow
these directions to install it.
Clone and Prepare a Virtual Environment
The poetry install
command installs all development dependencies into the
virtual environment (venv).
$ git clone https://github.com/byllyfish/finsy.git
$ cd finsy
$ python3 -m venv .venv
$ poetry install
Run Unit Tests
When you run pytest from the top level of the repository, you will run the unit tests.
$ poetry run pytest
Run Integration Tests
When you run pytest from within the examples
directory, you will run the integration
tests instead of the unit tests. The integration tests run the example programs against a
Mininet network. Docker or podman are required.
$ cd examples
$ poetry run pytest