Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[P4Testgen] Implement meter support for the BMv2 V1model PTF test back end #3974

Merged
merged 10 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 51 additions & 88 deletions backends/bmv2/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
import logging
import queue
import re
import socket
import sys
import threading
import time
from collections import Counter
from functools import partial, wraps
from functools import partialmethod, wraps
from pathlib import Path

import google.protobuf.text_format
Expand All @@ -30,15 +29,6 @@
import testutils


# See https://gist.github.com/carymrobbins/8940382
# functools.partialmethod is introduced in Python 3.4
class partialmethod(partial):
def __get__(self, instance, owner):
if instance is None:
return self
return partial(self.func, instance, *(self.args or ()), **(self.keywords or {}))


def stringify(n, length=0):
"""Take a non-negative integer 'n' as the first parameter, and a
non-negative integer 'length' in units of _bytes_ as the second
Expand All @@ -63,76 +53,6 @@ def stringify(n, length=0):
return s


def ipv4_to_binary(addr):
"""Take an argument 'addr' containing an IPv4 address written as a
string in dotted decimal notation, e.g. '10.1.2.3', and convert it
to a string with binary contents expected by the Python P4Runtime
client operations."""
bytes_ = [int(b, 10) for b in addr.split(".")]
assert len(bytes_) == 4
# Note: The bytes() call below will throw exception if any
# elements of bytes_ is outside of the range [0, 255]], so no need
# to add a separate check for that here.
return bytes(bytes_)


def ipv4_to_int(addr):
"""Take an argument 'addr' containing an IPv4 address written as a
string in dotted decimal notation, e.g. '10.1.2.3', and convert it
to an integer."""
bytes_ = [int(b, 10) for b in addr.split(".")]
assert len(bytes_) == 4
# Note: The bytes() call below will throw exception if any
# elements of bytes_ is outside of the range [0, 255]], so no need
# to add a separate check for that here.
return int.from_bytes(bytes(bytes_), byteorder="big")


def ipv6_to_binary(addr):
"""Take an argument 'addr' containing an IPv6 address written in
standard syntax, e.g. '2001:0db8::3210', and convert it to a
string with binary contents expected by the Python P4Runtime
client operations."""
return socket.inet_pton(socket.AF_INET6, addr)


def ipv6_to_int(addr):
"""Take an argument 'addr' containing an IPv6 address written in
standard syntax, e.g. '2001:0db8::3210', and convert it to an
integer."""
bytes_ = socket.inet_pton(socket.AF_INET6, addr)
# Note: The bytes() call below will throw exception if any
# elements of bytes_ is outside of the range [0, 255]], so no need
# to add a separate check for that here.
return int.from_bytes(bytes_, byteorder="big")


def mac_to_binary(addr):
"""Take an argument 'addr' containing an Ethernet MAC address written
as a string in hexadecimal notation, with each byte separated by a
colon, e.g. '00:de:ad:be:ef:ff', and convert it to a string with
binary contents expected by the Python P4Runtime client
operations."""
bytes_ = [int(b, 16) for b in addr.split(":")]
assert len(bytes_) == 6
# Note: The bytes() call below will throw exception if any
# elements of bytes_ is outside of the range [0, 255]], so no need
# to add a separate check for that here.
return bytes(bytes_)


def mac_to_int(addr):
"""Take an argument 'addr' containing an Ethernet MAC address written
as a string in hexadecimal notation, with each byte separated by a
colon, e.g. '00:de:ad:be:ef:ff', and convert it to an integer."""
bytes_ = [int(b, 16) for b in addr.split(":")]
assert len(bytes_) == 6
# Note: The bytes() call below will throw exception if any
# elements of bytes_ is outside of the range [0, 255]], so no need
# to add a separate check for that here.
return int.from_bytes(bytes(bytes_), byteorder="big")


# Used to indicate that the gRPC error Status object returned by the server has
# an incorrect format.
class P4RuntimeErrorFormatException(Exception):
Expand Down Expand Up @@ -283,6 +203,7 @@ def setUp(self):
self.dataplane = ptf.dataplane_instance
self.dataplane.flush()

self.p4info_obj_map = {}
self._swports = []
for _, port, _ in config["interfaces"]:
self._swports.append(port)
Expand Down Expand Up @@ -344,7 +265,6 @@ def updateConfig(self):
# In order to make writing tests easier, we accept any suffix that uniquely
# identifies the object among p4info objects of the same type.
def import_p4info_names(self):
self.p4info_obj_map = {}
suffix_count = Counter()
for obj_type in [
"tables",
Expand All @@ -353,6 +273,8 @@ def import_p4info_names(self):
"counters",
"direct_counters",
"controller_packet_metadata",
"meters",
"direct_meters",
]:
for obj in getattr(self.p4info, obj_type):
pre = obj.preamble
Expand Down Expand Up @@ -834,7 +756,7 @@ def send_request_add_entry_to_action(self, t_name, mk, a_name, params, priority=
req = p4runtime_pb2.WriteRequest()
req.device_id = self.device_id
self.push_update_add_entry_to_action(req, t_name, mk, a_name, params, priority)
return req, self.write_request(req, store=(mk is not None))
return req, self.write_request(req, store=mk is not None)

def check_table_name_and_key(self, table_name_and_key):
assert isinstance(table_name_and_key, tuple)
Expand Down Expand Up @@ -893,7 +815,7 @@ def table_add(self, table_name_and_key, action_name_and_params, priority=None, o
if key is None:
update.type = p4runtime_pb2.Update.MODIFY
testutils.log.info(f"table_add: req={req}")
return req, self.write_request(req, store=(key is not None))
return req, self.write_request(req, store=key is not None)

def pre_add_mcast_group(self, mcast_grp_id, port_instance_pair_list):
"""When a packet is sent from ingress to the packet buffer with
Expand Down Expand Up @@ -985,6 +907,37 @@ def counter_dump_data(self, counter_name, direct=False):
counter_entries.append(entry)
return counter_entries

def meter_write(self, meter_name, index, meter_config):
req = self.get_new_write_request()
update = req.updates.add()
update.type = p4runtime_pb2.Update.MODIFY
entity = update.entity
meter_write = entity.meter_entry
meter_write.meter_id = self.get_meter_id(meter_name)
meter_write.index.index = index
meter_write.config.cir = meter_config.cir
meter_write.config.cburst = meter_config.cburst
meter_write.config.pir = meter_config.pir
meter_write.config.pburst = meter_config.pburst
return req, self.write_request(req)

def direct_meter_write(self, meter_config, table_id, table_entry):
req = self.get_new_write_request()
update = req.updates.add()
update.type = p4runtime_pb2.Update.MODIFY
entity = update.entity
meter_write = entity.direct_meter_entry
meter_write.table_entry.table_id = table_id
if table_entry is None:
meter_write.table_entry.is_default_action = True
else:
meter_write.table_entry.CopyFrom(table_entry)
meter_write.config.cir = meter_config.cir
meter_write.config.cburst = meter_config.cburst
meter_write.config.pir = meter_config.pir
meter_write.config.pburst = meter_config.pburst
return req, self.write_request(req)

def make_table_read_request(self, table_name):
req = p4runtime_pb2.ReadRequest()
req.device_id = self.device_id
Expand All @@ -993,6 +946,14 @@ def make_table_read_request(self, table_name):
table.table_id = self.get_table_id(table_name)
return req, table

def make_table_read_request_by_id(self, table_id):
req = p4runtime_pb2.ReadRequest()
req.device_id = self.device_id
entity = req.entities.add()
table = entity.table_entry
table.table_id = table_id
return req, table

def table_dump_data(self, table_name):
req, table = self.make_table_read_request(table_name)
table_entries = []
Expand Down Expand Up @@ -1043,7 +1004,7 @@ def send_request_add_entry_to_member(self, t_name, mk, mbr_id):
req = p4runtime_pb2.WriteRequest()
req.device_id = self.device_id
self.push_update_add_entry_to_member(req, t_name, mk, mbr_id)
return req, self.write_request(req, store=(mk is not None))
return req, self.write_request(req, store=mk is not None)

def push_update_add_entry_to_group(self, req, t_name, mk, grp_id):
update = req.updates.add()
Expand All @@ -1060,7 +1021,7 @@ def send_request_add_entry_to_group(self, t_name, mk, grp_id):
req = p4runtime_pb2.WriteRequest()
req.device_id = self.device_id
self.push_update_add_entry_to_group(req, t_name, mk, grp_id)
return req, self.write_request(req, store=(mk is not None))
return req, self.write_request(req, store=mk is not None)

# iterates over all requests in reverse order; if they are INSERT updates,
# replay them as DELETE updates; this is a convenient way to clean-up a lot
Expand Down Expand Up @@ -1103,6 +1064,8 @@ def get_new_write_request(self):
("actions", "action"),
("counters", "counter"),
("direct_counters", "direct_counter"),
("meters", "meter"),
("direct_meters", "direct_meter"),
("controller_packet_metadata", "controller_packet_metadata"),
]:
name = "_".join(["get", nickname])
Expand Down Expand Up @@ -1151,7 +1114,7 @@ def update_config(config_path, p4info_path, grpc_addr, device_id):
"""
channel = grpc.insecure_channel(grpc_addr)
stub = p4runtime_pb2_grpc.P4RuntimeStub(channel)
testutils.log.info(f"Sending P4 config from file {config_path} with P4info {p4info_path}")
testutils.log.info("Sending P4 config from file %s with P4info %s", config_path, p4info_path)
request = p4runtime_pb2.SetForwardingPipelineConfigRequest()
request.device_id = device_id
config = request.config
Expand All @@ -1162,7 +1125,7 @@ def update_config(config_path, p4info_path, grpc_addr, device_id):
request.action = p4runtime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_COMMIT
try:
response = stub.SetForwardingPipelineConfig(request)
logging.debug(f"Response {response}")
logging.debug("Response %s", response)
except Exception as e:
logging.error("Error during SetForwardingPipelineConfig")
logging.error(e)
Expand Down
9 changes: 7 additions & 2 deletions backends/bmv2/run-bmv2-ptf-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,18 @@ def run_test(options: Options) -> int:
return result


def create_options(test_args) -> Options:
def create_options(test_args) -> testutils.Optional[Options]:
"""Parse the input arguments and create a processed options object."""
options = Options()
options.p4_file = Path(testutils.check_if_file(test_args.p4_file))
testfile = test_args.testfile
if not testfile:
testutils.log.info("No test file provided. Checking for file in folder.")
testfile = options.p4_file.with_suffix(".py")
options.testfile = Path(testutils.check_if_file(testfile))
result = testutils.check_if_file(testfile)
if not result:
return None
options.testfile = Path(result)
testdir = test_args.testdir
if not testdir:
testutils.log.info("No test directory provided. Generating temporary folder.")
Expand Down Expand Up @@ -240,6 +243,8 @@ def create_options(test_args) -> Options:
args, argv = PARSER.parse_known_args()

test_options = create_options(args)
if not test_options:
sys.exit(testutils.FAILURE)
# Run the test with the extracted options
test_result = run_test(test_options)
if not (args.nocleanup or test_result != testutils.SUCCESS):
Expand Down
2 changes: 1 addition & 1 deletion backends/p4tools/modules/testgen/lib/execution_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ const std::stack<std::reference_wrapper<const ExecutionState::StackFrame>>
}

void ExecutionState::setProperty(cstring propertyName, Continuation::PropertyValue property) {
stateProperties[propertyName] = std::move(property);
stateProperties[propertyName] = property;
}

bool ExecutionState::hasProperty(cstring propertyName) const {
Expand Down
5 changes: 3 additions & 2 deletions backends/p4tools/modules/testgen/targets/bmv2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ set(
${CMAKE_CURRENT_SOURCE_DIR}/constants.cpp
${CMAKE_CURRENT_SOURCE_DIR}/contrib/bmv2_hash/calculations.cpp
${CMAKE_CURRENT_SOURCE_DIR}/expr_stepper.cpp
${CMAKE_CURRENT_SOURCE_DIR}/map_direct_externs.cpp
${CMAKE_CURRENT_SOURCE_DIR}/p4_asserts_parser.cpp
${CMAKE_CURRENT_SOURCE_DIR}/p4_refers_to_parser.cpp
${CMAKE_CURRENT_SOURCE_DIR}/program_info.cpp
${CMAKE_CURRENT_SOURCE_DIR}/table_stepper.cpp
${CMAKE_CURRENT_SOURCE_DIR}/target.cpp
${CMAKE_CURRENT_SOURCE_DIR}/p4_asserts_parser.cpp
${CMAKE_CURRENT_SOURCE_DIR}/p4_refers_to_parser.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_backend.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_spec.cpp
PARENT_SCOPE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ inja::json Metadata::getVerify(const TestSpec *testSpec) {
inja::json verifyData = inja::json::object();
auto egressPacket = testSpec->getEgressPacket();
if (egressPacket.has_value()) {
const auto *const packet = egressPacket.value();
const auto *packet = egressPacket.value();
verifyData["eg_port"] = packet->getPort();
const auto *payload = packet->getEvaluatedPayload();
const auto *payloadMask = packet->getEvaluatedPayloadMask();
Expand Down Expand Up @@ -94,7 +94,7 @@ statement_coverage: {{coverage}}
## endfor

# The input packet in hexadecimal.
input_packet: \"{{send.pkt}}\"
input_packet: "{{send.pkt}}"

# Parsed headers and their offsets.
header_offsets:
Expand All @@ -105,7 +105,7 @@ input_packet: \"{{send.pkt}}\"
# Metadata results after this test has completed.
metadata:
## for metadata_field in metadata_fields
{{metadata_field.name}}: [value: \"{{metadata_field.value}}\", offset: {{metadata_field.offset}}]
{{metadata_field.name}}: [value: "{{metadata_field.value}}", offset: {{metadata_field.offset}}]
## endfor
)""");
return TEST_CASE;
Expand Down
Loading