Skip to content

Commit

Permalink
Fun with meters.
Browse files Browse the repository at this point in the history
  • Loading branch information
fruffy committed Apr 7, 2023
1 parent 0bac561 commit b440099
Show file tree
Hide file tree
Showing 14 changed files with 648 additions and 233 deletions.
122 changes: 30 additions & 92 deletions backends/bmv2/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
# https://github.com/p4lang/PI/blob/ec6865edc770b42f22fea15e6da17ca58a83d3a6/proto/ptf/base_test.py.

from collections import Counter
from functools import wraps, partial
from functools import wraps, partialmethod
import logging
import re
import socket
import sys
import threading
import time
Expand All @@ -31,17 +30,6 @@
sys.path.append(str(TOOLS_PATH))
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 @@ -65,77 +53,6 @@ def stringify(n, length=0):
s = n.to_bytes(length, byteorder='big')
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 @@ -285,6 +202,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 @@ -346,11 +264,10 @@ 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", "action_profiles", "actions", "counters", "direct_counters",
"controller_packet_metadata"
"controller_packet_metadata", "meters", "direct_meters",
]:
for obj in getattr(self.p4info, obj_type):
pre = obj.preamble
Expand Down Expand Up @@ -830,7 +747,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 @@ -890,7 +807,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 @@ -982,6 +899,26 @@ 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, direct):
req = self.get_new_write_request()
update = req.updates.add()
update.type = p4runtime_pb2.Update.MODIFY
entity = update.entity
if direct:
meter_write = entity.direct_meter_entry
meter_obj = self.get_obj("direct_meters", meter_name)
meter_write.table_entry.table_id = meter_obj.direct_table_id
meter_write.table_entry.match.add()
else:
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 make_table_read_request(self, table_name):
req = p4runtime_pb2.ReadRequest()
req.device_id = self.device_id
Expand Down Expand Up @@ -1040,7 +977,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 @@ -1057,7 +994,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 @@ -1096,6 +1033,7 @@ def get_new_write_request(self):
# get_obj("tables", x) and get_obj_id("tables", x)
for obj_type, nickname in [("tables", "table"), ("action_profiles", "ap"), ("actions", "action"),
("counters", "counter"), ("direct_counters", "direct_counter"),
("meters", "meter"), ("direct_meters", "direct_meter"),
("controller_packet_metadata", "controller_packet_metadata")]:
name = "_".join(["get", nickname])
setattr(P4RuntimeTest, name, partialmethod(P4RuntimeTest.get_obj, obj_type))
Expand Down Expand Up @@ -1144,7 +1082,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 @@ -1155,7 +1093,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
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 @@ -182,7 +182,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
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,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 @@ -92,7 +92,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 @@ -103,7 +103,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
74 changes: 58 additions & 16 deletions backends/p4tools/modules/testgen/targets/bmv2/backend/ptf/ptf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@ inja::json::array_t PTF::getClone(const std::map<cstring, const TestObject *> &c
return cloneJson;
}

inja::json::array_t getMeter(const std::map<cstring, const TestObject *> &meterValues) {
auto meterJson = inja::json::array_t();
for (auto meterValueInfoTuple : meterValues) {
const auto *meterValue = meterValueInfoTuple.second->checkedTo<Bmv2V1ModelMeterValue>();

const auto meterEntries = meterValue->unravelMap();
for (const auto &meterEntry : meterEntries) {
inja::json meterInfoJson;
meterInfoJson["name"] = meterValueInfoTuple.first;
meterInfoJson["value"] = formatHexExpr(meterEntry.second.second);
meterInfoJson["index"] = formatHex(meterEntry.first, meterEntry.second.first);
if (meterValue->isDirectMeter()) {
meterInfoJson["is_direct"] = "True";
} else {
meterInfoJson["is_direct"] = "False";
}
meterJson.push_back(meterInfoJson);
}
}
return meterJson;
}

std::vector<std::pair<size_t, size_t>> PTF::getIgnoreMasks(const IR::Constant *mask) {
std::vector<std::pair<size_t, size_t>> ignoreMasks;
if (mask == nullptr) {
Expand Down Expand Up @@ -184,11 +206,12 @@ inja::json PTF::getSend(const TestSpec *testSpec) {

inja::json PTF::getVerify(const TestSpec *testSpec) {
inja::json verifyData = inja::json::object();
if (testSpec->getEgressPacket() != std::nullopt) {
const auto &packet = **testSpec->getEgressPacket();
verifyData["eg_port"] = packet.getPort();
const auto *payload = packet.getEvaluatedPayload();
const auto *payloadMask = packet.getEvaluatedPayloadMask();
auto egressPacket = testSpec->getEgressPacket();
if (egressPacket.has_value()) {
const auto *packet = egressPacket.value();
verifyData["eg_port"] = packet->getPort();
const auto *payload = packet->getEvaluatedPayload();
const auto *payloadMask = packet->getEvaluatedPayloadMask();
verifyData["ignore_masks"] = getIgnoreMasks(payloadMask);

auto dataStr = formatHexExpr(payload, false, true, false);
Expand All @@ -197,14 +220,12 @@ inja::json PTF::getVerify(const TestSpec *testSpec) {
return verifyData;
}

static std::string getPreamble() {
void PTF::emitPreamble() {
static const std::string PREAMBLE(
R"""(# P4Runtime PTF test for {{test_name}}
# p4testgen seed: {{ default(seed, "none") }}
import logging
import sys
import os
from enum import Enum
from ptf.mask import Mask
Expand All @@ -214,7 +235,9 @@ from ptf import testutils as ptfutils
import base_test as bt
class AbstractTest(bt.P4RuntimeTest):
EnumColor = Enum("EnumColor", ["GREEN", "YELLOW", "RED"], start=0)
@bt.autocleanup
def setUp(self):
Expand All @@ -241,19 +264,33 @@ class AbstractTest(bt.P4RuntimeTest):
bt.testutils.log.info("Verifying Packet ...")
self.verifyPackets()
def meter_write_with_predefined_config(self, meter_name, index, value, direct):
"""Since we can not blast the target with packets, we have to carefully craft an artificial scenario where the meter will return the color we want. We do this by setting the meter config in such a way that the meter is forced to assign the desired color. For example, for RED to the lowest threshold values, to force a RED assignment."""
value = self.EnumColor(value)
if value == self.EnumColor.GREEN:
meter_config = bt.p4runtime_pb2.MeterConfig(
cir=4294967295, cburst=4294967295, pir=4294967295, pburst=4294967295
)
elif value == self.EnumColor.YELLOW:
meter_config = bt.p4runtime_pb2.MeterConfig(
cir=1, cburst=1, pir=4294967295, pburst=4294967295
)
elif value == self.EnumColor.RED:
meter_config = bt.p4runtime_pb2.MeterConfig(
cir=1, cburst=1, pir=1, pburst=1
)
else:
raise self.failureException(f"Unsupported meter value {value}")
return self.meter_write(meter_name, index, meter_config, direct)
)""");
return PREAMBLE;
}

void PTF::emitPreamble(const std::string &preamble) {
inja::json dataJson;
dataJson["test_name"] = basePath.stem();
if (seed) {
dataJson["seed"] = *seed;
}

inja::render_to(ptfFileStream, preamble, dataJson);
inja::render_to(ptfFileStream, PREAMBLE, dataJson);
ptfFileStream.flush();
}

Expand Down Expand Up @@ -314,6 +351,10 @@ class Test{{test_id}}(AbstractTest):
self.insert_pre_clone_session({{clone_info.session_id}}, [{{clone_info.clone_port}}])
## endfor
## endif
## for meter_value in meter_values
self.meter_write_with_predefined_config("{{meter_value.name}}", {{meter_value.index}}, {{meter_value.value}}, {{meter_value.is_direct}})
## endfor
def sendPacket(self):
## if send
Expand Down Expand Up @@ -379,6 +420,8 @@ void PTF::emitTestcase(const TestSpec *testSpec, cstring selectedBranches, size_
if (!cloneInfos.empty()) {
dataJson["clone_infos"] = getClone(cloneInfos);
}
auto meterValues = testSpec->getTestObjectCategory("meter_values");
dataJson["meter_values"] = getMeter(meterValues);

LOG5("PTF backend: emitting testcase:" << std::setw(4) << dataJson);

Expand All @@ -392,8 +435,7 @@ void PTF::outputTest(const TestSpec *testSpec, cstring selectedBranches, size_t
auto ptfFile = basePath;
ptfFile.replace_extension(".py");
ptfFileStream = std::ofstream(ptfFile);
std::string preamble = getPreamble();
emitPreamble(preamble);
emitPreamble();
preambleEmitted = true;
}
std::string testCase = getTestCaseTemplate();
Expand Down
Loading

0 comments on commit b440099

Please sign in to comment.