Fields
In P4 parlance, a field is typically an integer constrained by a bit width, and is either a member of a key field for a “match table”, or a value passed as an action parameter when a key field is matched.
In order to present a field value to any element mentioned previously, you would first have to construct a gRPC field object, and then serialise an integer value into a byte array. I would argue that this is inconvenient to do, and that they can be presented in a far more obvious and human readable way.
This library provides a Field
object that can be derived from to define
data types constrained by a bit width, checking whether a value used to
construct an instance is representable by this constraint, and lends itself to
type checks further down the line.
An example in this library is the Layer2Port
field. It represents a
source or destination port in a layer 3 protocol such is ICMP, TCP or UDP.
from bfrt_helper.fields import Layer2Port
port = Layer2Port(80)
And this is defined simply as:
from bfrt_helper.fields import Field
class Layer2Port(Field):
bitwidth = 16
The only thing required is that the bit width is specified. Python magic does all the rest.
Existing Fields
The following fields are defined already:
DevPort
DigestType
EgressSpec
IPv4Address
Layer2Port
MACAddress
MulticastGroupId
MulticastNodeId
PortId
ReplicationId
StringField
VlanID
Defining Custom Fields
If fields are represented by a simple integer value, then they can be defined
as in the previous example. However, some fields have more interesting
expressions, such as an IPv4Address
or MACAddress
. In this case
you will need to overload the constructor, the deserialisation method
from_bytes
, and optionally the __str__
method.
An example of this is indeed an IPv4Address
:
class IPv4Address(Field):
bitwidth = 32
def __init__(self, address: str):
super().__init__(int(ipaddress.ip_address(address)))
def __str__(self):
return str(ipaddress.ip_address(self.value))
@classmethod
def from_bytes(cls, data):
return cls(ipaddress.ip_address(data).__str__())
The constructor is overloaded as the underlying value type is required to be an
integer, so we use the ipaddress
Python library to convert a human readable
address into a number.
The reason for overloading the __str__
method should be obvious; when it is
printed you will again have nice readable expression.
Finally, the from_bytes
function is required to deserialise data from the
device.
Another example is MACAddress
:
class MACAddress(Field):
bitwidth = 48
def __init__(self, address):
if isinstance(address, int):
super().__init__(address)
else:
super().__init__(int(address.replace(":", ""), 16))
def __str__(self):
return ":".join([f"{b:02x}" for b in self.value.to_bytes(6, 16)])
@classmethod
def from_bytes(cls, data):
return cls(":".join([f"{b:02x}" for b in data.to_bytes(6, 16)]))
For some reason I decided to allow the address to be supplied as in integer. I can’t for the life of me remember why.
Operations on Fields
Fields have convenience operators defined for comparisons. The operators available are:
==
(equality)!=
(inequality)&
(bitwise AND)|
(bitwise OR)^
(bitwise XOR)<=
(less than or equal to)<
(less than)>=
(greater than or equal to)>
(greater than)
Additionally, a hash is available via hash(field)
(__hash__
).
The use cases for most are straightforward, however an interesting case is
perform masking operations such as on an IPv4Address
:
from bfrt_helper.fields import IPv4Address
addr = IPv4Address('192.168.0.46')
mask = IPv4Address('255.255.255.0')
masked = addr & mask
expected = IPv4Address('192.168.0.0')
assert masked == expected
assert str(masked) == '192.168.0.0'
Yes, this could be done with native libraries such as the ipaddress module, but again this is to provide similar operations natively.
More of these operators can be added, with reference to the Python data model.
Note
Currently, any comparison operator performs a strict test against their types. However, such operators could reasonably expected to work against fields with the same bit width. The view is that the current position is sensible, but we are open to changing this. It is almost certainly better to start more strict, as any code which would rely on the alternative semantics would surely fail on a change.