diff --git a/src/device/requirements.in b/src/device/requirements.in index 10506fbd42c5b7a64afb3cc7c6ea32e0f1fa49f6..9c8c0ef18f3bcd4a92180465d11cd465c4336d44 100644 --- a/src/device/requirements.in +++ b/src/device/requirements.in @@ -10,6 +10,9 @@ pytz==2021.3 redis==4.1.2 requests==2.27.1 xmltodict==0.12.0 +tabulate +ipaddress +macaddress # pip's dependency resolver does not take into account installed packages. # p4runtime does not specify the version of grpcio/protobuf it needs, so it tries to install latest one diff --git a/src/device/service/drivers/p4/__init__.py b/src/device/service/drivers/p4/__init__.py index 70a33251242c51f49140e596b8208a19dd5245f7..9953c820575d42fa88351cc8de022d880ba96e6a 100644 --- a/src/device/service/drivers/p4/__init__.py +++ b/src/device/service/drivers/p4/__init__.py @@ -11,4 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - diff --git a/src/device/service/drivers/p4/p4_client.py b/src/device/service/drivers/p4/p4_client.py new file mode 100644 index 0000000000000000000000000000000000000000..600d08880c7e8a1d6a7238e60d66a87d7167bd8c --- /dev/null +++ b/src/device/service/drivers/p4/p4_client.py @@ -0,0 +1,607 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +P4Runtime client. +""" + +import logging +import queue +import sys +import enum +import threading +from functools import wraps +from typing import NamedTuple +import grpc +import google.protobuf.text_format +from google.rpc import status_pb2, code_pb2 + +from p4.v1 import p4runtime_pb2 +from p4.v1 import p4runtime_pb2_grpc + +STREAM_ATTR_ARBITRATION = "arbitration" +STREAM_ATTR_PACKET = "packet" +STREAM_ATTR_DIGEST = "digest" +STREAM_ATTR_IDLE_NOT = "idle_timeout_notification" +STREAM_ATTR_UNKNOWN = "unknown" + +LOGGER = logging.getLogger(__name__) + + +class P4RuntimeErrorFormatException(Exception): + """ + P4Runtime error format exception. + """ + + +# Used to iterate over the p4.Error messages in a gRPC error Status object +class P4RuntimeErrorIterator: + """ + P4Runtime error iterator. + + Attributes + ---------- + grpc_error : object + gRPC error + """ + + def __init__(self, grpc_error): + assert grpc_error.code() == grpc.StatusCode.UNKNOWN + self.grpc_error = grpc_error + + error = None + # The gRPC Python package does not have a convenient way to access the + # binary details for the error: they are treated as trailing metadata. + for meta in self.grpc_error.trailing_metadata(): + if meta[0] == "grpc-status-details-bin": + error = status_pb2.Status() + error.ParseFromString(meta[1]) + break + if error is None: + raise P4RuntimeErrorFormatException("No binary details field") + + if len(error.details) == 0: + raise P4RuntimeErrorFormatException( + "Binary details field has empty Any details repeated field") + self.errors = error.details + self.idx = 0 + + def __iter__(self): + return self + + def __next__(self): + while self.idx < len(self.errors): + p4_error = p4runtime_pb2.Error() + one_error_any = self.errors[self.idx] + if not one_error_any.Unpack(p4_error): + raise P4RuntimeErrorFormatException( + "Cannot convert Any message to p4.Error") + if p4_error.canonical_code == code_pb2.OK: + continue + val = self.idx, p4_error + self.idx += 1 + return val + raise StopIteration + + +class P4RuntimeWriteException(Exception): + """ + P4Runtime write exception handler. + + Attributes + ---------- + grpc_error : object + gRPC error + """ + + def __init__(self, grpc_error): + assert grpc_error.code() == grpc.StatusCode.UNKNOWN + super().__init__() + self.errors = [] + try: + error_iterator = P4RuntimeErrorIterator(grpc_error) + for error_tuple in error_iterator: + self.errors.append(error_tuple) + except P4RuntimeErrorFormatException as ex: + raise P4RuntimeException(grpc_error) from ex + + def __str__(self): + message = "Error(s) during Write:\n" + for idx, p4_error in self.errors: + code_name = code_pb2._CODE.values_by_number[ + p4_error.canonical_code].name + message += f"\t* At index {idx}: {code_name}, " \ + f"'{p4_error.message}'\n" + return message + + +class P4RuntimeException(Exception): + """ + P4Runtime exception handler. + + Attributes + ---------- + grpc_error : object + gRPC error + """ + + def __init__(self, grpc_error): + super().__init__() + self.grpc_error = grpc_error + + def __str__(self): + message = f"P4Runtime RPC error ({self.grpc_error.code().name}): " \ + f"{self.grpc_error.details()}" + return message + + +def parse_p4runtime_write_error(func): + """ + Parse P4Runtime write error. + + :param func: function + :return: parsed error + """ + + @wraps(func) + def handle(*args, **kwargs): + try: + return func(*args, **kwargs) + except grpc.RpcError as ex: + if ex.code() != grpc.StatusCode.UNKNOWN: + raise ex + raise P4RuntimeWriteException(ex) from None + + return handle + + +def parse_p4runtime_error(func): + """ + Parse P4Runtime error. + + :param func: function + :return: parsed error + """ + + @wraps(func) + def handle(*args, **kwargs): + try: + return func(*args, **kwargs) + except grpc.RpcError as ex: + raise P4RuntimeException(ex) from None + + return handle + + +class SSLOptions(NamedTuple): + """ + Tuple of SSL options. + """ + insecure: bool + cacert: str = None + cert: str = None + key: str = None + + +def read_pem_file(path): + """ + Load and read PEM file. + + :param path: path to PEM file + :return: file descriptor + """ + try: + with open(path, "rb") as f_d: + return f_d.read() + except (FileNotFoundError, IOError, OSError): + logging.critical("Cannot read from PEM file '%s'", path) + sys.exit(1) + + +@enum.unique +class WriteOperation(enum.Enum): + """ + Write Operations. + """ + insert = 1 + update = 2 + delete = 3 + + +def select_operation(mode): + """ + Select P4 operation based upon the operation mode. + + :param mode: operation mode + :return: P4 operation protobuf object + """ + if mode == WriteOperation.insert: + return p4runtime_pb2.Update.INSERT + if mode == WriteOperation.update: + return p4runtime_pb2.Update.UPDATE + if mode == WriteOperation.delete: + return p4runtime_pb2.Update.DELETE + return None + + +def select_entity_type(entity, update): + """ + Select P4 entity type for an update. + + :param entity: P4 entity object + :param update: update operation + :return: the correct update entity or None + """ + if isinstance(entity, p4runtime_pb2.TableEntry): + return update.entity.table_entry + if isinstance(entity, p4runtime_pb2.ActionProfileGroup): + return update.entity.action_profile_group + if isinstance(entity, p4runtime_pb2.ActionProfileMember): + return update.entity.action_profile_member + return None + + +class P4RuntimeClient: + """ + P4Runtime client. + + Attributes + ---------- + device_id : int + P4 device ID + grpc_address : str + IP address and port + election_id : tuple + Mastership election ID + role_name : str + Role name (optional) + ssl_options: tuple + SSL options" named tuple (optional) + """ + + def __init__(self, device_id, grpc_address, + election_id, role_name=None, ssl_options=None): + self.device_id = device_id + self.election_id = election_id + self.role_name = role_name + if ssl_options is None: + self.ssl_options = SSLOptions(True) + else: + self.ssl_options = ssl_options + LOGGER.debug( + "Connecting to device %d at %s", device_id, grpc_address) + + if self.ssl_options.insecure: + logging.debug("Using insecure channel") + self.channel = grpc.insecure_channel(grpc_address) + else: + # root certificates are retrieved from a default location + # chosen by gRPC runtime unless the user provides + # custom certificates. + root_certificates = None + if self.ssl_options.cacert is not None: + root_certificates = read_pem_file(self.ssl_options.cacert) + certificate_chain = None + if self.ssl_options.cert is not None: + certificate_chain = read_pem_file(self.ssl_options.cert) + private_key = None + if self.ssl_options.key is not None: + private_key = read_pem_file(self.ssl_options.key) + creds = grpc.ssl_channel_credentials(root_certificates, private_key, + certificate_chain) + self.channel = grpc.secure_channel(grpc_address, creds) + self.stream_in_q = None + self.stream_out_q = None + self.stream = None + self.stream_recv_thread = None + self.stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel) + + try: + self.set_up_stream() + except P4RuntimeException: + LOGGER.critical("Failed to connect to P4Runtime server") + sys.exit(1) + LOGGER.info("P4Runtime client is successfully invoked") + + def set_up_stream(self): + """ + Set up a gRPC stream. + """ + self.stream_out_q = queue.Queue() + # queues for different messages + self.stream_in_q = { + STREAM_ATTR_ARBITRATION: queue.Queue(), + STREAM_ATTR_PACKET: queue.Queue(), + STREAM_ATTR_DIGEST: queue.Queue(), + STREAM_ATTR_IDLE_NOT: queue.Queue(), + STREAM_ATTR_UNKNOWN: queue.Queue(), + } + + def stream_req_iterator(): + while True: + stream_p = self.stream_out_q.get() + if stream_p is None: + break + yield stream_p + + def stream_recv_wrapper(stream): + @parse_p4runtime_error + def stream_recv(): + for stream_p in stream: + if stream_p.HasField("arbitration"): + self.stream_in_q["arbitration"].put(stream_p) + elif stream_p.HasField("packet"): + self.stream_in_q["packet"].put(stream_p) + elif stream_p.HasField("digest"): + self.stream_in_q["digest"].put(stream_p) + else: + self.stream_in_q["unknown"].put(stream_p) + + try: + stream_recv() + except P4RuntimeException as ex: + logging.critical("StreamChannel error, closing stream") + logging.critical(ex) + for k in self.stream_in_q: + self.stream_in_q[k].put(None) + + self.stream = self.stub.StreamChannel(stream_req_iterator()) + self.stream_recv_thread = threading.Thread( + target=stream_recv_wrapper, args=(self.stream,)) + self.stream_recv_thread.start() + self.handshake() + + def handshake(self): + """ + Handshake with gRPC server. + """ + + req = p4runtime_pb2.StreamMessageRequest() + arbitration = req.arbitration + arbitration.device_id = self.device_id + election_id = arbitration.election_id + election_id.high = self.election_id[0] + election_id.low = self.election_id[1] + if self.role_name is not None: + arbitration.role.name = self.role_name + self.stream_out_q.put(req) + + rep = self.get_stream_packet(STREAM_ATTR_ARBITRATION, timeout=2) + if rep is None: + logging.critical("Failed to establish session with server") + sys.exit(1) + is_primary = (rep.arbitration.status.code == code_pb2.OK) + logging.debug("Session established, client is '%s'", + "primary" if is_primary else "backup") + if not is_primary: + print("You are not the primary client," + "you only have read access to the server") + + def get_stream_packet(self, type_, timeout=1): + """ + Get a new message from the stream. + + :param type_: stream type. + :param timeout: time to wait. + :return: message or None + """ + if type_ not in self.stream_in_q: + print("Unknown stream type 's"'', type_) + return None + try: + msg = self.stream_in_q[type_].get(timeout=timeout) + return msg + except queue.Empty: # timeout expired + return None + + @parse_p4runtime_error + def get_p4info(self): + """ + Retrieve P4Info content. + + :return: P4Info object. + """ + logging.debug("Retrieving P4Info file") + req = p4runtime_pb2.GetForwardingPipelineConfigRequest() + req.device_id = self.device_id + req.response_type = \ + p4runtime_pb2.GetForwardingPipelineConfigRequest.P4INFO_AND_COOKIE + rep = self.stub.GetForwardingPipelineConfig(req) + return rep.config.p4info + + @parse_p4runtime_error + def set_fwd_pipe_config(self, p4info_path, bin_path): + """ + Configure the pipeline. + + :param p4info_path: path to the P4Info file + :param bin_path: path to the binary file + :return: + """ + logging.debug("Setting forwarding pipeline config") + req = p4runtime_pb2.SetForwardingPipelineConfigRequest() + req.device_id = self.device_id + if self.role_name is not None: + req.role = self.role_name + election_id = req.election_id + election_id.high = self.election_id[0] + election_id.low = self.election_id[1] + req.action = \ + p4runtime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_COMMIT + with open(p4info_path, "r", encoding="utf-8") as f_info: + with open(bin_path, "rb") as f_bin: + try: + google.protobuf.text_format.Merge( + f_info.read(), req.config.p4info) + except google.protobuf.text_format.ParseError: + logging.error("Error when parsing P4Info") + raise + req.config.p4_device_config = f_bin.read() + return self.stub.SetForwardingPipelineConfig(req) + + def tear_down(self): + """ + Tear connection with the gRPC server down. + """ + if self.stream_out_q: + logging.debug("Cleaning up stream") + self.stream_out_q.put(None) + if self.stream_in_q: + for k in self.stream_in_q: + self.stream_in_q[k].put(None) + if self.stream_recv_thread: + self.stream_recv_thread.join() + self.channel.close() + # avoid a race condition if channel deleted when process terminates + del self.channel + + @parse_p4runtime_write_error + def __write(self, entity, mode=WriteOperation.insert): + """ + Perform a write operation. + + :param entity: P4 entity to write + :param mode: operation mode (defaults to insert) + :return: void + """ + if isinstance(entity, (list, tuple)): + for ent in entity: + self.__write(ent) + return + req = self.__get_new_write_request() + update = req.updates.add() + update.type = select_operation(mode) + msg_entity = select_entity_type(entity, update) + if not msg_entity: + msg = f"{mode.name} operation for entity {entity.__name__}" \ + f"not supported" + raise P4RuntimeWriteException(msg) + msg_entity.CopyFrom(entity) + self.__simple_write(req) + + def __get_new_write_request(self): + """ + Create a new write request message. + + :return: write request message + """ + req = p4runtime_pb2.WriteRequest() + req.device_id = self.device_id + if self.role_name is not None: + req.role = self.role_name + election_id = req.election_id + election_id.high = self.election_id[0] + election_id.low = self.election_id[1] + return req + + @parse_p4runtime_write_error + def __simple_write(self, req): + """ + Send a write operation into the wire. + + :param req: write operation request + :return: void + """ + try: + return self.stub.Write(req) + except grpc.RpcError as ex: + if ex.code() != grpc.StatusCode.UNKNOWN: + raise ex + raise P4RuntimeWriteException(ex) from ex + + @parse_p4runtime_write_error + def insert(self, entity): + """ + Perform an insert write operation. + + :param entity: P4 entity to insert + :return: void + """ + return self.__write(entity, WriteOperation.insert) + + @parse_p4runtime_write_error + def update(self, entity): + """ + Perform an update write operation. + + :param entity: P4 entity to update + :return: void + """ + return self.__write(entity, WriteOperation.update) + + @parse_p4runtime_write_error + def delete(self, entity): + """ + Perform a delete write operation. + + :param entity: P4 entity to delete + :return: void + """ + return self.__write(entity, WriteOperation.delete) + + @parse_p4runtime_write_error + def write(self, req): + """ + Write device operation. + + :param req: write request message + :return: status + """ + req.device_id = self.device_id + if self.role_name is not None: + req.role = self.role_name + election_id = req.election_id + election_id.high = self.election_id[0] + election_id.low = self.election_id[1] + return self.__simple_write(req) + + @parse_p4runtime_write_error + def write_update(self, update): + """ + Update device operation. + + :param update: update request message + :return: status + """ + req = self.__get_new_write_request() + req.updates.extend([update]) + return self.__simple_write(req) + + # Decorator is useless here: in case of server error, + # the exception is raised during the iteration (when next() is called). + @parse_p4runtime_error + def read_one(self, entity): + """ + Read device operation. + + :param entity: P4 entity for which the read is issued + :return: status + """ + req = p4runtime_pb2.ReadRequest() + if self.role_name is not None: + req.role = self.role_name + req.device_id = self.device_id + req.entities.extend([entity]) + return self.stub.Read(req) + + @parse_p4runtime_error + def api_version(self): + """ + P4Runtime API version. + + :return: API version hex + """ + req = p4runtime_pb2.CapabilitiesRequest() + rep = self.stub.Capabilities(req) + return rep.p4runtime_api_version diff --git a/src/device/service/drivers/p4/p4_common.py b/src/device/service/drivers/p4/p4_common.py new file mode 100644 index 0000000000000000000000000000000000000000..bcafedc1f613bfe1d1739d72f89803155b720155 --- /dev/null +++ b/src/device/service/drivers/p4/p4_common.py @@ -0,0 +1,445 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This package contains several helper functions for encoding to and decoding from +byte strings: +- integers +- IPv4 address strings +- IPv6 address strings +- Ethernet address strings +as well as static variables used by various P4 driver components. +""" + +import logging +import math +import re +import socket +import ipaddress +from ctypes import c_uint16, sizeof +import macaddress + +from common.type_checkers.Checkers import chk_type +try: + from .p4_exception import UserBadValueError +except ImportError: + from p4_exception import UserBadValueError + +P4_ATTR_DEV_ID = "id" +P4_ATTR_DEV_NAME = "name" +P4_ATTR_DEV_VENDOR = "vendor" +P4_ATTR_DEV_HW_VER = "hw_ver" +P4_ATTR_DEV_SW_VER = "sw_ver" +P4_ATTR_DEV_P4BIN = "p4bin" +P4_ATTR_DEV_P4INFO = "p4info" +P4_ATTR_DEV_TIMEOUT = "timeout" + +P4_VAL_DEF_VENDOR = "Unknown" +P4_VAL_DEF_HW_VER = "BMv2 simple_switch" +P4_VAL_DEF_SW_VER = "Stratum" +P4_VAL_DEF_TIMEOUT = 60 + + +# Logger instance +LOGGER = logging.getLogger(__name__) + + +# MAC address encoding/decoding +mac_pattern = re.compile(r"^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$") + + +def matches_mac(mac_addr_string): + """ + Check whether input string is a valid MAC address or not. + + :param mac_addr_string: string-based MAC address + :return: boolean status + """ + return mac_pattern.match(mac_addr_string) is not None + + +def encode_mac(mac_addr_string): + """ + Convert string-based MAC address into bytes. + + :param mac_addr_string: string-based MAC address + :return: MAC address in bytes + """ + return bytes(macaddress.MAC(mac_addr_string)) + + +def decode_mac(encoded_mac_addr): + """ + Convert a MAC address in bytes into string-based MAC address. + + :param encoded_mac_addr: MAC address in bytes + :return: string-based MAC address + """ + return str(macaddress.MAC(encoded_mac_addr)).replace("-", ":").lower() + + +# IP address encoding/decoding +IPV4_LOCALHOST = "localhost" + + +def matches_ipv4(ip_addr_string): + """ + Check whether input string is a valid IPv4 address or not. + + :param ip_addr_string: string-based IPv4 address + :return: boolean status + """ + if ip_addr_string == IPV4_LOCALHOST: + return True + try: + addr = ipaddress.ip_address(ip_addr_string) + return isinstance(addr, ipaddress.IPv4Address) + except ValueError: + return False + + +def encode_ipv4(ip_addr_string): + """ + Convert string-based IPv4 address into bytes. + + :param ip_addr_string: string-based IPv4 address + :return: IPv4 address in bytes + """ + return socket.inet_aton(ip_addr_string) + + +def decode_ipv4(encoded_ip_addr): + """ + Convert an IPv4 address in bytes into string-based IPv4 address. + + :param encoded_ip_addr: IPv4 address in bytes + :return: string-based IPv4 address + """ + return socket.inet_ntoa(encoded_ip_addr) + + +def matches_ipv6(ip_addr_string): + """ + Check whether input string is a valid IPv6 address or not. + + :param ip_addr_string: string-based IPv6 address + :return: boolean status + """ + try: + addr = ipaddress.ip_address(ip_addr_string) + return isinstance(addr, ipaddress.IPv6Address) + except ValueError: + return False + + +def encode_ipv6(ip_addr_string): + """ + Convert string-based IPv6 address into bytes. + + :param ip_addr_string: string-based IPv6 address + :return: IPv6 address in bytes + """ + return socket.inet_pton(socket.AF_INET6, ip_addr_string) + + +def decode_ipv6(encoded_ip_addr): + """ + Convert an IPv6 address in bytes into string-based IPv6 address. + + :param encoded_ip_addr: IPv6 address in bytes + :return: string-based IPv6 address + """ + return str(ipaddress.ip_address(encoded_ip_addr)) + + +# Numerical encoding/decoding + + +def limits(c_int_type): + """ + Discover limits of numerical type. + + :param c_int_type: numerical type + :return: tuple of numerical type's limits + """ + signed = c_int_type(-1).value < c_int_type(0).value + bit_size = sizeof(c_int_type) * 8 + signed_limit = 2 ** (bit_size - 1) + return (-signed_limit, signed_limit - 1) \ + if signed else (0, 2 * signed_limit - 1) + + +def valid_port(port): + """ + Check whether input is a valid port number or not. + + :param port: port number + :return: boolean status + """ + lim = limits(c_uint16) + return lim[0] <= port <= lim[1] + + +def bitwidth_to_bytes(bitwidth): + """ + Convert number of bits to number of bytes. + + :param bitwidth: number of bits + :return: number of bytes + """ + return int(math.ceil(bitwidth / 8.0)) + + +def encode_num(number, bitwidth): + """ + Convert number into bytes. + + :param number: number to convert + :param bitwidth: number of bits + :return: number in bytes + """ + byte_len = bitwidth_to_bytes(bitwidth) + return number.to_bytes(byte_len, byteorder="big") + + +def decode_num(encoded_number): + """ + Convert number in bytes into its numerical form. + + :param encoded_number: number in bytes to convert + :return: numerical number form + """ + return int.from_bytes(encoded_number, "big") + + +# Umbrella encoder + + +def encode(variable, bitwidth): + """ + Tries to infer the type of `input` and encode it. + + :param variable: target variable + :param bitwidth: size of variable in bits + :return: encoded bytes + """ + byte_len = bitwidth_to_bytes(bitwidth) + if isinstance(variable, (list, tuple)) and len(variable) == 1: + variable = variable[0] + + if isinstance(variable, int): + encoded_bytes = encode_num(variable, bitwidth) + elif isinstance(variable, str): + if matches_mac(variable): + encoded_bytes = encode_mac(variable) + elif matches_ipv4(variable): + encoded_bytes = encode_ipv4(variable) + elif matches_ipv6(variable): + encoded_bytes = encode_ipv6(variable) + else: + try: + value = int(variable, 0) + except ValueError as ex: + raise UserBadValueError( + f"Invalid value '{variable}': " + "could not cast to integer, try in hex with 0x prefix")\ + from ex + encoded_bytes = value.to_bytes(byte_len, byteorder="big") + else: + raise Exception( + f"Encoding objects of {type(variable)} is not supported") + assert len(encoded_bytes) == byte_len + return encoded_bytes + + +# Parsers + + +def get_match_field_value(match_field): + """ + Retrieve the value of a certain match field by name. + + :param match_field: match field + :return: match filed value + """ + match_type = match_field.WhichOneof("field_match_type") + if match_type == "valid": + return match_field.valid.value + if match_type == "exact": + return match_field.exact.value + if match_type == "lpm": + return match_field.lpm.value, match_field.lpm.prefix_len + if match_type == "ternary": + return match_field.ternary.value, match_field.ternary.mask + if match_type == "range": + return match_field.range.low, match_field.range.high + raise Exception(f"Unsupported match type with type {match_type}") + + +def parse_resource_string_from_json(resource, resource_str="table-name"): + """ + Parse a given resource name within a JSON-based object. + + :param resource: JSON-based object + :param resource_str: resource string to parse + :return: value of the parsed resource string + """ + if not resource or (resource_str not in resource): + LOGGER.warning("JSON entry misses '%s' attribute", resource_str) + return None + chk_type(resource_str, resource[resource_str], str) + return resource[resource_str] + + +def parse_resource_number_from_json(resource, resource_nb): + """ + Parse a given resource number within a JSON-based object. + + :param resource: JSON-based object + :param resource_nb: resource number to parse + :return: value of the parsed resource number + """ + if not resource or (resource_nb not in resource): + LOGGER.warning( + "JSON entry misses '%s' attribute", resource_nb) + return None + chk_type(resource_nb, resource[resource_nb], int) + return resource[resource_nb] + + +def parse_resource_integer_from_json(resource, resource_nb): + """ + Parse a given integer number within a JSON-based object. + + :param resource: JSON-based object + :param resource_nb: resource number to parse + :return: value of the parsed resource number + """ + num = parse_resource_number_from_json(resource, resource_nb) + if num: + return int(num) + return -1 + + +def parse_resource_float_from_json(resource, resource_nb): + """ + Parse a given floating point number within a JSON-based object. + + :param resource: JSON-based object + :param resource_nb: resource number to parse + :return: value of the parsed resource number + """ + num = parse_resource_number_from_json(resource, resource_nb) + if num: + return float(num) + return -1.0 + + +def parse_resource_bytes_from_json(resource, resource_bytes): + """ + Parse given resource bytes within a JSON-based object. + + :param resource: JSON-based object + :param resource_bytes: resource bytes to parse + :return: value of the parsed resource bytes + """ + if not resource or (resource_bytes not in resource): + LOGGER.debug( + "JSON entry misses '%s' attribute", resource_bytes) + return None + + if resource_bytes in resource: + chk_type(resource_bytes, resource[resource_bytes], bytes) + return resource[resource_bytes] + return None + + +def parse_match_operations_from_json(resource): + """ + Parse the match operations within a JSON-based object. + + :param resource: JSON-based object + :return: map of match operations + """ + if not resource or ("match-fields" not in resource): + LOGGER.warning( + "JSON entry misses 'match-fields' list of attributes") + return {} + chk_type("match-fields", resource["match-fields"], list) + + match_map = {} + for mf_entry in resource["match-fields"]: + if ("match-field" not in mf_entry) or \ + ("match-value" not in mf_entry): + LOGGER.warning( + "JSON entry misses 'match-field' and/or " + "'match-value' attributes") + return None + chk_type("match-field", mf_entry["match-field"], str) + chk_type("match-value", mf_entry["match-value"], str) + match_map[mf_entry["match-field"]] = mf_entry["match-value"] + + return match_map + + +def parse_action_parameters_from_json(resource): + """ + Parse the action parameters within a JSON-based object. + + :param resource: JSON-based object + :return: map of action parameters + """ + if not resource or ("action-params" not in resource): + LOGGER.warning( + "JSON entry misses 'action-params' list of attributes") + return None + chk_type("action-params", resource["action-params"], list) + + action_name = parse_resource_string_from_json(resource, "action-name") + + action_params = {} + for ac_entry in resource["action-params"]: + if not ac_entry: + LOGGER.debug( + "Missing action parameter for action %s", action_name) + continue + chk_type("action-param", ac_entry["action-param"], str) + chk_type("action-value", ac_entry["action-value"], str) + action_params[ac_entry["action-param"]] = \ + ac_entry["action-value"] + + return action_params + + +def parse_integer_list_from_json(resource, resource_list, resource_item): + """ + Parse the list of integers within a JSON-based object. + + :param resource: JSON-based object + :param resource_list: name of the resource list + :param resource_item: name of the resource item + :return: list of integers + """ + if not resource or (resource_list not in resource): + LOGGER.warning( + "JSON entry misses '%s' list of attributes", resource_list) + return [] + chk_type(resource_list, resource[resource_list], list) + + integers_list = [] + for item in resource[resource_list]: + chk_type(resource_item, item[resource_item], int) + integers_list.append(item[resource_item]) + + return integers_list diff --git a/src/device/service/drivers/p4/p4_context.py b/src/device/service/drivers/p4/p4_context.py new file mode 100644 index 0000000000000000000000000000000000000000..ab01c422fe478cfe26c2f7331fc9b4653521db9f --- /dev/null +++ b/src/device/service/drivers/p4/p4_context.py @@ -0,0 +1,284 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Build some context around a given P4 info file. +""" + +from collections import Counter +import enum +from functools import partialmethod + + +@enum.unique +class P4Type(enum.Enum): + """ + P4 types. + """ + table = 1 + action = 2 + action_profile = 3 + counter = 4 + direct_counter = 5 + meter = 6 + direct_meter = 7 + controller_packet_metadata = 8 + + +P4Type.table.p4info_name = "tables" +P4Type.action.p4info_name = "actions" +P4Type.action_profile.p4info_name = "action_profiles" +P4Type.counter.p4info_name = "counters" +P4Type.direct_counter.p4info_name = "direct_counters" +P4Type.meter.p4info_name = "meters" +P4Type.direct_meter.p4info_name = "direct_meters" +P4Type.controller_packet_metadata.p4info_name = "controller_packet_metadata" + +for object_type in P4Type: + object_type.pretty_name = object_type.name.replace('_', ' ') + object_type.pretty_names = object_type.pretty_name + 's' + + +@enum.unique +class P4RuntimeEntity(enum.Enum): + """ + P4 runtime entities. + """ + table_entry = 1 + action_profile_member = 2 + action_profile_group = 3 + meter_entry = 4 + direct_meter_entry = 5 + counter_entry = 6 + direct_counter_entry = 7 + packet_replication_engine_entry = 8 + + +class Context: + """ + P4 context. + """ + def __init__(self): + self.p4info = None + self.p4info_obj_map = {} + self.p4info_obj_map_by_id = {} + self.p4info_objs_by_type = {} + + def set_p4info(self, p4info): + """ + Set a p4 info file. + + :param p4info: p4 info file + :return: void + """ + self.p4info = p4info + self._import_p4info_names() + + def get_obj(self, obj_type, name): + """ + Retrieve an object by type and name. + + :param obj_type: P4 object type + :param name: P4 object name + :return: P4 object + """ + key = (obj_type, name) + return self.p4info_obj_map.get(key, None) + + def get_obj_id(self, obj_type, name): + """ + Retrieve a P4 object's ID by type and name. + + :param obj_type: P4 object type + :param name: P4 object name + :return: P4 object ID + """ + obj = self.get_obj(obj_type, name) + if obj is None: + return None + return obj.preamble.id + + def get_param(self, action_name, name): + """ + Get an action parameter by action name. + + :param action_name: P4 action name + :param name: action parameter name + :return: action parameter + """ + action = self.get_obj(P4Type.action, action_name) + if action is None: + return None + for param in action.params: + if param.name == name: + return param + return None + + def get_mf(self, table_name, name): + """ + Get a table's match field by name. + + :param table_name: P4 table name + :param name: match field name + :return: match field + """ + table = self.get_obj(P4Type.table, table_name) + if table is None: + return None + for match_field in table.match_fields: + if match_field.name == name: + return match_field + return None + + def get_param_id(self, action_name, name): + """ + Get an action parameter ID by the action and parameter names. + + :param action_name: P4 action name + :param name: action parameter name + :return: action parameter ID + """ + param = self.get_param(action_name, name) + return None if param is None else param.id + + def get_mf_id(self, table_name, name): + """ + Get a table's match field ID by name. + + :param table_name: P4 table name + :param name: match field name + :return: match field ID + """ + match_field = self.get_mf(table_name, name) + return None if match_field is None else match_field.id + + def get_param_name(self, action_name, id_): + """ + Get an action parameter name by the action name and action ID. + + :param action_name: P4 action name + :param id_: action parameter ID + :return: action parameter name + """ + action = self.get_obj(P4Type.action, action_name) + if action is None: + return None + for param in action.params: + if param.id == id_: + return param.name + return None + + def get_mf_name(self, table_name, id_): + """ + Get a table's match field name by ID. + + :param table_name: P4 table name + :param id_: match field ID + :return: match field name + """ + table = self.get_obj(P4Type.table, table_name) + if table is None: + return None + for match_field in table.match_fields: + if match_field.id == id_: + return match_field.name + return None + + def get_objs(self, obj_type): + """ + Get P4 objects by type. + + :param obj_type: P4 object type + :return: list of tuples (object name, object) + """ + objects = self.p4info_objs_by_type[obj_type] + for name, obj in objects.items(): + yield name, obj + + def get_name_from_id(self, id_): + """ + Get P4 object name by its ID. + + :param id_: P4 object ID + :return: P4 object name + """ + return self.p4info_obj_map_by_id[id_].preamble.name + + def get_obj_by_id(self, id_): + """ + Get P4 object by its ID. + + :param id_: P4 object ID + :return: P4 object + """ + return self.p4info_obj_map_by_id[id_] + + def get_packet_metadata_name_from_id(self, ctrl_pkt_md_name, id_): + """ + Get packet metadata name by ID. + + :param ctrl_pkt_md_name: packet replication entity name + :param id_: packet metadata ID + :return: packet metadata name + """ + ctrl_pkt_md = self.get_obj( + P4Type.controller_packet_metadata, ctrl_pkt_md_name) + if not ctrl_pkt_md: + return None + for meta in ctrl_pkt_md.metadata: + if meta.id == id_: + return meta.name + return None + + # We accept any suffix that uniquely identifies the object + # among p4info objects of the same type. + def _import_p4info_names(self): + """ + Import p4 info into memory. + + :return: void + """ + suffix_count = Counter() + for obj_type in P4Type: + self.p4info_objs_by_type[obj_type] = {} + for obj in getattr(self.p4info, obj_type.p4info_name): + pre = obj.preamble + self.p4info_obj_map_by_id[pre.id] = obj + self.p4info_objs_by_type[obj_type][pre.name] = obj + suffix = None + for suf in reversed(pre.name.split(".")): + suffix = suf if suffix is None else suf + "." + suffix + key = (obj_type, suffix) + self.p4info_obj_map[key] = obj + suffix_count[key] += 1 + for key, cnt in suffix_count.items(): + if cnt > 1: + del self.p4info_obj_map[key] + + +# Add p4info object and object id "getters" for each object type; +# these are just wrappers around Context.get_obj and Context.get_obj_id. +# For example: get_table(x) and get_table_id(x) respectively call +# get_obj(P4Type.table, x) and get_obj_id(P4Type.table, x) +for object_type in P4Type: + object_name = "_".join(["get", object_type.name]) + setattr(Context, object_name, partialmethod( + Context.get_obj, object_type)) + object_name = "_".join(["get", object_type.name, "id"]) + setattr(Context, object_name, partialmethod( + Context.get_obj_id, object_type)) + +for object_type in P4Type: + object_name = "_".join(["get", object_type.p4info_name]) + setattr(Context, object_name, partialmethod(Context.get_objs, object_type)) diff --git a/src/device/service/drivers/p4/p4_driver.py b/src/device/service/drivers/p4/p4_driver.py index af05952b313d1632eacd5962cc34c4aa1b6b5a10..069c07ce40e43192b74519b2175e7e10c638cd20 100644 --- a/src/device/service/drivers/p4/p4_driver.py +++ b/src/device/service/drivers/p4/p4_driver.py @@ -16,13 +16,22 @@ P4 driver plugin for the TeraFlow SDN controller. """ +import os +import json import logging import threading from typing import Any, Iterator, List, Optional, Tuple, Union -from .p4_util import P4RuntimeClient,\ +from common.type_checkers.Checkers import chk_type, chk_length, chk_string +from .p4_common import matches_ipv4, matches_ipv6, valid_port,\ P4_ATTR_DEV_ID, P4_ATTR_DEV_NAME, P4_ATTR_DEV_VENDOR,\ - P4_ATTR_DEV_HW_VER, P4_ATTR_DEV_SW_VER, P4_ATTR_DEV_PIPECONF,\ - P4_VAL_DEF_VENDOR, P4_VAL_DEF_HW_VER, P4_VAL_DEF_SW_VER, P4_VAL_DEF_PIPECONF + P4_ATTR_DEV_HW_VER, P4_ATTR_DEV_SW_VER,\ + P4_ATTR_DEV_P4BIN, P4_ATTR_DEV_P4INFO, P4_ATTR_DEV_TIMEOUT,\ + P4_VAL_DEF_VENDOR, P4_VAL_DEF_HW_VER, P4_VAL_DEF_SW_VER,\ + P4_VAL_DEF_TIMEOUT +from .p4_manager import P4Manager, get_api_version, KEY_TABLE,\ + KEY_ACTION_PROFILE, KEY_COUNTER, KEY_DIR_COUNTER, KEY_METER, KEY_DIR_METER,\ + KEY_CTL_PKT_METADATA +from .p4_client import WriteOperation try: from _Driver import _Driver @@ -53,208 +62,543 @@ class P4Driver(_Driver): Hardware version of the P4 device (Optional) sw_ver : str Software version of the P4 device (Optional) - pipeconf : str - P4 device table configuration (Optional) + p4bin : str + Path to P4 binary file (Optional, but must be combined with p4info) + p4info : str + Path to P4 info file (Optional, but must be combined with p4bin) + timeout : int + Device timeout in seconds (Optional) """ def __init__(self, address: str, port: int, **settings) -> None: # pylint: disable=super-init-not-called - self.__client = None + self.__manager = None self.__address = address self.__port = int(port) + self.__endpoint = None self.__settings = settings - - try: - self.__dev_id = self.__settings.get(P4_ATTR_DEV_ID) - except Exception as ex: - LOGGER.error('P4 device ID is a mandatory setting') - raise Exception from ex - - if P4_ATTR_DEV_NAME in self.__settings: - self.__dev_name = self.__settings.get(P4_ATTR_DEV_NAME) - else: - self.__dev_name = str(self.__dev_id) - LOGGER.warning( - 'No device name is provided. Setting default name: %s', - self.__dev_name) - - if P4_ATTR_DEV_VENDOR in self.__settings: - self.__dev_vendor = self.__settings.get(P4_ATTR_DEV_VENDOR) - else: - self.__dev_vendor = P4_VAL_DEF_VENDOR - LOGGER.warning( - 'No vendor is provided. Setting default vendor: %s', - self.__dev_vendor) - - if P4_ATTR_DEV_HW_VER in self.__settings: - self.__dev_hw_version = self.__settings.get(P4_ATTR_DEV_HW_VER) - else: - self.__dev_hw_version = P4_VAL_DEF_HW_VER - LOGGER.warning( - 'No HW version is provided. Setting default HW version: %s', - self.__dev_hw_version) - - if P4_ATTR_DEV_SW_VER in self.__settings: - self.__dev_sw_version = self.__settings.get(P4_ATTR_DEV_SW_VER) - else: - self.__dev_sw_version = P4_VAL_DEF_SW_VER - LOGGER.warning( - 'No SW version is provided. Setting default SW version: %s', - self.__dev_sw_version) - - if P4_ATTR_DEV_PIPECONF in self.__settings: - self.__dev_pipeconf = self.__settings.get(P4_ATTR_DEV_PIPECONF) - else: - self.__dev_pipeconf = P4_VAL_DEF_PIPECONF - LOGGER.warning( - 'No P4 pipeconf is provided. Setting default P4 pipeconf: %s', - self.__dev_pipeconf) - + self.__id = None + self.__name = None + self.__vendor = P4_VAL_DEF_VENDOR + self.__hw_version = P4_VAL_DEF_HW_VER + self.__sw_version = P4_VAL_DEF_SW_VER + self.__p4bin_path = None + self.__p4info_path = None + self.__timeout = P4_VAL_DEF_TIMEOUT self.__lock = threading.Lock() self.__started = threading.Event() self.__terminate = threading.Event() - LOGGER.info('Initializing P4 device at %s:%d with settings:', + self.__parse_and_validate_settings() + + LOGGER.info("Initializing P4 device at %s:%d with settings:", self.__address, self.__port) for key, value in settings.items(): - LOGGER.info('\t%8s = %s', key, value) + LOGGER.info("\t%8s = %s", key, value) def Connect(self) -> bool: """ - Establishes a connection between the P4 device driver and a P4 device. + Establish a connection between the P4 device driver and a P4 device. :return: boolean connection status. """ - LOGGER.info( - 'Connecting to P4 device %s:%d ...', - self.__address, self.__port) + LOGGER.info("Connecting to P4 device %s ...", self.__endpoint) with self.__lock: # Skip if already connected if self.__started.is_set(): return True - # Instantiate a gRPC channel with the P4 device - grpc_address = f'{self.__address}:{self.__port}' + # Dynamically devise an election ID election_id = (1, 0) - self.__client = P4RuntimeClient( - self.__dev_id, grpc_address, election_id) - LOGGER.info('\tConnected!') + + # Spawn a P4 manager for this device + self.__manager = P4Manager( + device_id=self.__id, + ip_address=self.__address, + port=self.__port, + election_id=election_id) + assert self.__manager + + # Start the P4 manager + try: + self.__manager.start(self.__p4bin_path, self.__p4info_path) + except Exception as ex: # pylint: disable=broad-except + raise Exception(ex) from ex + + LOGGER.info("\tConnected via P4Runtime version %s", + get_api_version()) self.__started.set() return True def Disconnect(self) -> bool: """ - Terminates the connection between the P4 device driver and a P4 device. + Terminate the connection between the P4 device driver and a P4 device. :return: boolean disconnection status. """ - LOGGER.info( - 'Disconnecting from P4 device %s:%d ...', - self.__address, self.__port) + LOGGER.info("Disconnecting from P4 device %s ...", self.__endpoint) # If not started, assume it is already disconnected if not self.__started.is_set(): return True - # gRPC client must already be instantiated - assert self.__client + # P4 manager must already be instantiated + assert self.__manager # Trigger termination of loops and processes self.__terminate.set() # Trigger connection tear down with the P4Runtime server - self.__client.tear_down() - self.__client = None + self.__manager.stop() + self.__manager = None - LOGGER.info('\tDisconnected!') + LOGGER.info("\tDisconnected!") return True def GetInitialConfig(self) -> List[Tuple[str, Any]]: """ - Retrieves the initial configuration of a P4 device. + Retrieve the initial configuration of a P4 device. :return: list of initial configuration items. """ - LOGGER.info('P4 GetInitialConfig()') - return [] + initial_conf = [] - def GetConfig(self, resource_keys : List[str] = [])\ + with self.__lock: + if not initial_conf: + LOGGER.warning("No initial configuration for P4 device %s ...", + self.__endpoint) + return [] + + def GetConfig(self, resource_keys: List[str] = [])\ -> List[Tuple[str, Union[Any, None, Exception]]]: """ - Retrieves the current configuration of a P4 device. + Retrieve the current configuration of a P4 device. - :param resource_keys: configuration parameters to retrieve. - :return: list of values associated with the requested resource keys. + :param resource_keys: P4 resource keys to retrieve. + :return: list of values associated with the requested resource keys or + None/Exception. """ + LOGGER.info( + "Getting configuration from P4 device %s ...", self.__endpoint) - LOGGER.info('P4 GetConfig()') - return [] + # No resource keys means fetch all configuration + if len(resource_keys) == 0: + LOGGER.warning( + "GetConfig with no resource keys " + "implies getting all resource keys!") + resource_keys = [ + obj_name for obj_name, _ in self.__manager.p4_objects.items() + ] + + # Verify the input type + chk_type("resources", resource_keys, list) + + with self.__lock: + return self.__get_resources(resource_keys) - def SetConfig(self, resources : List[Tuple[str, Any]])\ + def SetConfig(self, resources: List[Tuple[str, Any]])\ -> List[Union[bool, Exception]]: """ - Submits a new configuration to a P4 device. + Submit a new configuration to a P4 device. - :param resources: configuration parameters to set. - :return: list of results for resource key changes requested. + :param resources: P4 resources to set. + :return: list of boolean results or Exceptions for resource key + changes requested. """ - LOGGER.info('P4 SetConfig()') - return [] + LOGGER.info( + "Setting configuration to P4 device %s ...", self.__endpoint) - def DeleteConfig(self, resources : List[Tuple[str, Any]])\ + if not resources or len(resources) == 0: + LOGGER.warning( + "SetConfig requires a list of resources to store " + "into the device. Nothing is provided though.") + return [] + + assert isinstance(resources, list) + + with self.__lock: + return self.__set_resources(resources) + + def DeleteConfig(self, resources: List[Tuple[str, Any]])\ -> List[Union[bool, Exception]]: """ - Revokes P4 device configuration. + Revoke P4 device configuration. :param resources: list of tuples with resource keys to be deleted. - :return: list of results for resource key deletions requested. + :return: list of boolean results or Exceptions for resource key + deletions requested. """ - LOGGER.info('P4 DeleteConfig()') - return [] + LOGGER.info( + "Deleting configuration from P4 device %s ...", self.__endpoint) + + if not resources or len(resources) == 0: + LOGGER.warning( + "DeleteConfig requires a list of resources to delete " + "from the device. Nothing is provided though.") + return [] - def GetResource(self, endpoint_uuid : str) -> Optional[str]: + with self.__lock: + return self.__delete_resources(resources) + + def GetResource(self, endpoint_uuid: str) -> Optional[str]: """ - Retrieves a certain resource from a P4 device. + Retrieve a certain resource from a P4 device. :param endpoint_uuid: target endpoint UUID. :return: The path of the endpoint or None if not found. """ - LOGGER.info('P4 GetResource()') + LOGGER.warning("GetResource() RPC not yet implemented by the P4 driver") return "" - def GetState(self, blocking=False, terminate : Optional[threading.Event] = None) -> Iterator[Tuple[str, Any]]: + def GetState(self, + blocking=False, + terminate: Optional[threading.Event] = None) -> \ + Iterator[Tuple[str, Any]]: """ - Retrieves the state of a P4 device. + Retrieve the state of a P4 device. :param blocking: if non-blocking, the driver terminates the loop and returns. + :param terminate: termination flag. :return: sequences of state sample. """ - LOGGER.info('P4 GetState()') + LOGGER.warning("GetState() RPC not yet implemented by the P4 driver") return [] - def SubscribeState(self, subscriptions : List[Tuple[str, float, float]])\ + def SubscribeState(self, subscriptions: List[Tuple[str, float, float]])\ -> List[Union[bool, Exception]]: """ - Subscribes to certain state information. + Subscribe to certain state information. :param subscriptions: list of tuples with resources to be subscribed. :return: list of results for resource subscriptions requested. """ - LOGGER.info('P4 SubscribeState()') - return [] + LOGGER.warning( + "SubscribeState() RPC not yet implemented by the P4 driver") + return [False for _ in subscriptions] - def UnsubscribeState(self, subscriptions : List[Tuple[str, float, float]])\ + def UnsubscribeState(self, subscriptions: List[Tuple[str, float, float]])\ -> List[Union[bool, Exception]]: """ - Unsubscribes from certain state information. + Unsubscribe from certain state information. :param subscriptions: list of tuples with resources to be unsubscribed. :return: list of results for resource un-subscriptions requested. """ - LOGGER.info('P4 UnsubscribeState()') - return [] + LOGGER.warning( + "UnsubscribeState() RPC not yet implemented by the P4 driver") + return [False for _ in subscriptions] + + def get_manager(self): + """ + Get an instance of the P4 manager. + + :return: P4 manager instance + """ + return self.__manager + + def __parse_and_validate_settings(self): + """ + Verify that the driver inputs comply to what is expected. + + :return: void or exception in case of validation error + """ + # Device endpoint information + assert matches_ipv4(self.__address) or (matches_ipv6(self.__address)),\ + f"{self.__address} not a valid IPv4 or IPv6 address" + assert valid_port(self.__port), \ + f"{self.__port} not a valid transport port" + self.__endpoint = f"{self.__address}:{self.__port}" + + # Device ID + try: + self.__id = self.__settings.get(P4_ATTR_DEV_ID) + except Exception as ex: + LOGGER.error("P4 device ID is a mandatory setting") + raise Exception from ex + + # Device name + if P4_ATTR_DEV_NAME in self.__settings: + self.__name = self.__settings.get(P4_ATTR_DEV_NAME) + else: + self.__name = str(self.__id) + LOGGER.warning( + "No device name is provided. Setting default name: %s", + self.__name) + + # Device vendor + if P4_ATTR_DEV_VENDOR in self.__settings: + self.__vendor = self.__settings.get(P4_ATTR_DEV_VENDOR) + else: + LOGGER.warning( + "No device vendor is provided. Setting default vendor: %s", + self.__vendor) + + # Device hardware version + if P4_ATTR_DEV_HW_VER in self.__settings: + self.__hw_version = self.__settings.get(P4_ATTR_DEV_HW_VER) + else: + LOGGER.warning( + "No HW version is provided. Setting default HW version: %s", + self.__hw_version) + + # Device software version + if P4_ATTR_DEV_SW_VER in self.__settings: + self.__sw_version = self.__settings.get(P4_ATTR_DEV_SW_VER) + else: + LOGGER.warning( + "No SW version is provided. Setting default SW version: %s", + self.__sw_version) + + # Path to P4 binary file + if P4_ATTR_DEV_P4BIN in self.__settings: + self.__p4bin_path = self.__settings.get(P4_ATTR_DEV_P4BIN) + assert os.path.exists(self.__p4bin_path),\ + "Invalid path to p4bin file" + assert P4_ATTR_DEV_P4INFO in self.__settings,\ + "p4info and p4bin settings must be provided together" + + # Path to P4 info file + if P4_ATTR_DEV_P4INFO in self.__settings: + self.__p4info_path = self.__settings.get(P4_ATTR_DEV_P4INFO) + assert os.path.exists(self.__p4info_path),\ + "Invalid path to p4info file" + assert P4_ATTR_DEV_P4BIN in self.__settings,\ + "p4info and p4bin settings must be provided together" + + if (not self.__p4bin_path) or (not self.__p4info_path): + LOGGER.warning( + "No P4 binary and info files are provided, hence " + "no pipeline will be installed on the whitebox device.\n" + "This driver will attempt to manage whatever pipeline " + "is available on the target device.") + + # Device timeout + if P4_ATTR_DEV_TIMEOUT in self.__settings: + self.__timeout = self.__settings.get(P4_ATTR_DEV_TIMEOUT) + assert self.__timeout > 0,\ + "Device timeout must be a positive integer" + else: + LOGGER.warning( + "No device timeout is provided. Setting default timeout: %s", + self.__timeout) + + def __get_resources(self, resource_keys): + """ + Retrieve the current configuration of a P4 device. + + :param resource_keys: P4 resource keys to retrieve. + :return: list of values associated with the requested resource keys or + None/Exception. + """ + resources = [] + + LOGGER.debug("GetConfig() -> Keys: %s", resource_keys) + + for resource_key in resource_keys: + entries = [] + try: + if KEY_TABLE == resource_key: + for table_name in self.__manager.get_table_names(): + t_entries = self.__manager.table_entries_to_json( + table_name) + if t_entries: + entries.append(t_entries) + elif KEY_COUNTER == resource_key: + for cnt_name in self.__manager.get_counter_names(): + c_entries = self.__manager.counter_entries_to_json( + cnt_name) + if c_entries: + entries.append(c_entries) + elif KEY_DIR_COUNTER == resource_key: + for d_cnt_name in self.__manager.get_direct_counter_names(): + dc_entries = \ + self.__manager.direct_counter_entries_to_json( + d_cnt_name) + if dc_entries: + entries.append(dc_entries) + elif KEY_METER == resource_key: + for meter_name in self.__manager.get_meter_names(): + m_entries = self.__manager.meter_entries_to_json( + meter_name) + if m_entries: + entries.append(m_entries) + elif KEY_DIR_METER == resource_key: + for d_meter_name in self.__manager.get_direct_meter_names(): + dm_entries = \ + self.__manager.direct_meter_entries_to_json( + d_meter_name) + if dm_entries: + entries.append(dm_entries) + elif KEY_ACTION_PROFILE == resource_key: + for ap_name in self.__manager.get_action_profile_names(): + ap_entries = \ + self.__manager.action_prof_member_entries_to_json( + ap_name) + if ap_entries: + entries.append(ap_entries) + elif KEY_CTL_PKT_METADATA == resource_key: + msg = f"{resource_key.capitalize()} is not a " \ + f"retrievable resource" + raise Exception(msg) + else: + msg = f"GetConfig failed due to invalid " \ + f"resource key: {resource_key}" + raise Exception(msg) + resources.append( + (resource_key, entries if entries else None) + ) + except Exception as ex: # pylint: disable=broad-except + resources.append((resource_key, ex)) + + return resources + + def __set_resources(self, resources): + """ + Submit a new configuration to a P4 device. + + :param resources: P4 resources to set. + :return: list of boolean results or Exceptions for resource key + changes requested. + """ + results = [] + + for i, resource in enumerate(resources): + str_resource_name = f"resources[#{i}]" + resource_key = "" + try: + chk_type( + str_resource_name, resource, (list, tuple)) + chk_length( + str_resource_name, resource, min_length=2, max_length=2) + resource_key, resource_value = resource + chk_string( + str_resource_name, resource_key, allow_empty=False) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception( + "Exception validating %s: %s", + str_resource_name, str(resource_key)) + results.append(e) # store the exception if validation fails + continue + + try: + resource_value = json.loads(resource_value) + except Exception: # pylint: disable=broad-except + pass + + LOGGER.debug( + "SetConfig() -> Key: %s - Value: %s", + resource_key, resource_value) + + # Default operation is insert. + # P4 manager has internal logic to judge whether an entry + # to be inserted already exists, thus simply needs an update. + operation = WriteOperation.insert + + try: + self.__apply_operation(resource_key, resource_value, operation) + results.append(True) + except Exception as ex: # pylint: disable=broad-except + results.append(ex) + + print(results) + + return results + + def __delete_resources(self, resources): + """ + Revoke P4 device configuration. + + :param resources: list of tuples with resource keys to be deleted. + :return: list of boolean results or Exceptions for resource key + deletions requested. + """ + results = [] + + for i, resource in enumerate(resources): + str_resource_name = f"resources[#{i}]" + resource_key = "" + try: + chk_type( + str_resource_name, resource, (list, tuple)) + chk_length( + str_resource_name, resource, min_length=2, max_length=2) + resource_key, resource_value = resource + chk_string( + str_resource_name, resource_key, allow_empty=False) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception( + "Exception validating %s: %s", + str_resource_name, str(resource_key)) + results.append(e) # store the exception if validation fails + continue + + try: + resource_value = json.loads(resource_value) + except Exception: # pylint: disable=broad-except + pass + + LOGGER.debug("DeleteConfig() -> Key: %s - Value: %s", + resource_key, resource_value) + + operation = WriteOperation.delete + + try: + self.__apply_operation(resource_key, resource_value, operation) + results.append(True) + except Exception as ex: # pylint: disable=broad-except + results.append(ex) + + print(results) + + return results + + def __apply_operation( + self, resource_key, resource_value, operation: WriteOperation): + """ + Apply a write operation to a P4 resource. + + :param resource_key: P4 resource key + :param resource_value: P4 resource value in JSON format + :param operation: write operation (i.e., insert, update, delete) + to apply + :return: True if operation is successfully applied or raise Exception + """ + + # Apply settings to the various tables + if KEY_TABLE == resource_key: + self.__manager.table_entry_operation_from_json( + resource_value, operation) + elif KEY_COUNTER == resource_key: + self.__manager.counter_entry_operation_from_json( + resource_value, operation) + elif KEY_DIR_COUNTER == resource_key: + self.__manager.direct_counter_entry_operation_from_json( + resource_value, operation) + elif KEY_METER == resource_key: + self.__manager.meter_entry_operation_from_json( + resource_value, operation) + elif KEY_DIR_METER == resource_key: + self.__manager.direct_meter_entry_operation_from_json( + resource_value, operation) + elif KEY_ACTION_PROFILE == resource_key: + self.__manager.action_prof_member_entry_operation_from_json( + resource_value, operation) + self.__manager.action_prof_group_entry_operation_from_json( + resource_value, operation) + elif KEY_CTL_PKT_METADATA == resource_key: + msg = f"{resource_key.capitalize()} is not a " \ + f"configurable resource" + raise Exception(msg) + else: + msg = f"{operation} on invalid key {resource_key}" + LOGGER.error(msg) + raise Exception(msg) + + LOGGER.debug("%s operation: %s", resource_key.capitalize(), operation) + + return True diff --git a/src/device/service/drivers/p4/p4_exception.py b/src/device/service/drivers/p4/p4_exception.py new file mode 100644 index 0000000000000000000000000000000000000000..3e3afb723b3850fd9a9b2b1c4982bf8ae31b20f7 --- /dev/null +++ b/src/device/service/drivers/p4/p4_exception.py @@ -0,0 +1,135 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +P4 driver exceptions. +""" + + +class UserError(Exception): + """ + User error exception. + """ + def __init__(self, info=""): + super().__init__() + self.info = info + + def __str__(self): + return self.info + + # TODO: find better way to get a custom traceback # pylint: disable=W0511 + def _render_traceback_(self): + return [str(self)] + + +class InvalidP4InfoError(Exception): + """ + Invalid P4 info exception. + """ + def __init__(self, info=""): + super().__init__() + self.info = info + + def __str__(self): + return f"Invalid P4Info message: {self.info}" + + def _render_traceback_(self): + return [str(self)] + + +class UnknownOptionName(UserError): + """ + Unknown option name exception. + """ + def __init__(self, option_name): + super().__init__() + self.option_name = option_name + + def __str__(self): + return f"Unknown option name: {self.option_name}" + + +class InvalidOptionValueType(UserError): + """ + Invalid option value type exception. + """ + def __init__(self, option, value): + super().__init__() + self.option = option + self.value = value + + def __str__(self): + return f"Invalid value type for option {self.option.name}. "\ + "Expected {self.option.value.__name__} but got "\ + "value {self.value} with type {type(self.value).__name__}" + + +class UserBadIPv4Error(UserError): + """ + Invalid IPv4 address value exception. + """ + def __init__(self, addr): + super().__init__() + self.addr = addr + + def __str__(self): + return f"{self.addr}' is not a valid IPv4 address" + + def _render_traceback_(self): + return [str(self)] + + +class UserBadIPv6Error(UserError): + """ + Invalid IPv6 address value exception. + """ + def __init__(self, addr): + super().__init__() + self.addr = addr + + def __str__(self): + return f"'{self.addr}' is not a valid IPv6 address" + + def _render_traceback_(self): + return [str(self)] + + +class UserBadMacError(UserError): + """ + Invalid MAC address value exception. + """ + def __init__(self, addr): + super().__init__() + self.addr = addr + + def __str__(self): + return f"'{self.addr}' is not a valid MAC address" + + def _render_traceback_(self): + return [str(self)] + + +class UserBadValueError(UserError): + """ + Invalid value exception. + """ + def __init__(self, info=""): + super().__init__() + self.info = info + + def __str__(self): + return self.info + + def _render_traceback_(self): + return [str(self)] diff --git a/src/device/service/drivers/p4/p4_global_options.py b/src/device/service/drivers/p4/p4_global_options.py new file mode 100644 index 0000000000000000000000000000000000000000..86043b671e9316dfeff2fb12db8ab3088386382a --- /dev/null +++ b/src/device/service/drivers/p4/p4_global_options.py @@ -0,0 +1,204 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +P4Runtime global options. +""" + +import enum +try: + from .p4_exception import UnknownOptionName, InvalidOptionValueType +except ImportError: + from p4_exception import UnknownOptionName, InvalidOptionValueType + + +@enum.unique +class Options(enum.Enum): + """ + P4 options. + """ + canonical_bytestrings = bool + + +class GlobalOptions: + """ + P4 global options. + """ + option_defaults = { + Options.canonical_bytestrings: True, + } + + option_helpstrings = { + Options.canonical_bytestrings: """ +Use byte-padded legacy format for binary strings sent to the P4Runtime server, +instead of the canonical representation. See P4Runtime specification for details. +""" + } + + def __init__(self): + self._values = {} + self.reset() + self._option_names = [option.name for option in Options] + self._set_docstring() + + def reset(self): + """ + Reset all options to their defaults. + + :return: void + """ + for option in Options: + assert option in GlobalOptions.option_defaults + self._values[option] = GlobalOptions.option_defaults[option] + + def _supported_options_as_str(self): + """ + Return a comma-separated string of supported options. + + :return: string of supported options + """ + return ", ".join([f"{o.name} ({o.value.__name__})" for o in Options]) + + def _supported_options_as_str_verbose(self): + """ + Return a detailed comma-separated string of supported options. + + :return: string of supported options + """ + opt_str = "" + for option in Options: + opt_str += f"Option name: {option.name}\n" + opt_str += f"Type: {option.value.__name__}\n" + opt_str += f"Default value: " \ + f"{GlobalOptions.option_defaults[option]}\n" + opt_str += f"Description: " \ + f"{GlobalOptions.option_helpstrings.get(option, 'N/A')}\n" + opt_str += "\n" + return opt_str[:-1] + + def _set_docstring(self): + """ + Set the documentation for this object. + + :return: void + """ + self.__doc__ = f""" +Manage global options for the P4Runtime shell. +Supported options are: {self._supported_options_as_str()} +To set the value of a global option, use GLOBAL_OPTIONS["<option name>"] = <option value> +To access the current value of a global option, use GLOBAL_OPTIONS.["<option name>"] +To reset all options to their default value, use GLOBAL_OPTIONS.reset + +{self._supported_options_as_str_verbose()} +""" + + def __dir__(self): + """ + Return all names in this scope. + + :return: list of names in scope + """ + return ["reset", "set", "get"] + + def set_option(self, option, value): + """ + Set an option's value. + + :param option: option to set + :param value: option value + :return: void + """ + self._values[option] = value + + def get_option(self, option): + """ + Get an option's value. + + :param option: option to get + :return: option value + """ + return self._values[option] + + def set(self, name, value): + """ + Create an option and set its value. + + :param name: option name + :param value: option value + :return: void + """ + try: + option = Options[name] + except KeyError as ex: + raise UnknownOptionName(name) from ex + if not isinstance(value, option.value): + raise InvalidOptionValueType(option, value) + self.set_option(option, value) + + def get(self, name): + """ + Get option by name. + + :param name: option name + :return: option + """ + try: + option = Options[name] + except KeyError as ex: + raise UnknownOptionName(name) from ex + return self.get_option(option) + + def __setitem__(self, name, value): + self.set(name, value) + + def __getitem__(self, name): + return self.get(name) + + def __str__(self): + return '\n'.join([f"{o.name}: {v}" for o, v in self._values.items()]) + + +GLOBAL_OPTIONS = GlobalOptions() + + +def to_canonical_bytes(bytes_): + """ + Convert to canonical bytes. + + :param bytes_: byte stream + :return: canonical bytes + """ + if len(bytes_) == 0: + return bytes_ + num_zeros = 0 + for byte in bytes_: + if byte != 0: + break + num_zeros += 1 + if num_zeros == len(bytes_): + return bytes_[:1] + return bytes_[num_zeros:] + + +def make_canonical_if_option_set(bytes_): + """ + Convert to canonical bytes if option is set. + + :param bytes_: byte stream + :return: canonical bytes + """ + + if GLOBAL_OPTIONS.get_option(Options.canonical_bytestrings): + return to_canonical_bytes(bytes_) + return bytes_ diff --git a/src/device/service/drivers/p4/p4_manager.py b/src/device/service/drivers/p4/p4_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..dc25e80b5803bfdec7d802d41c136865f4c045e3 --- /dev/null +++ b/src/device/service/drivers/p4/p4_manager.py @@ -0,0 +1,5987 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +P4Runtime manager. +""" + +import enum +import os +import queue +import time +import logging +from collections import Counter, OrderedDict +from threading import Thread +from tabulate import tabulate +from p4.v1 import p4runtime_pb2 +from p4.config.v1 import p4info_pb2 + +try: + from .p4_client import P4RuntimeClient, P4RuntimeException,\ + P4RuntimeWriteException, WriteOperation, parse_p4runtime_error + from .p4_context import P4RuntimeEntity, P4Type, Context + from .p4_global_options import make_canonical_if_option_set + from .p4_common import encode,\ + parse_resource_string_from_json, parse_resource_integer_from_json,\ + parse_resource_bytes_from_json, parse_match_operations_from_json,\ + parse_action_parameters_from_json, parse_integer_list_from_json + from .p4_exception import UserError, InvalidP4InfoError +except ImportError: + from p4_client import P4RuntimeClient, P4RuntimeException,\ + P4RuntimeWriteException, WriteOperation, parse_p4runtime_error + from p4_context import P4RuntimeEntity, P4Type, Context + from p4_global_options import make_canonical_if_option_set + from p4_common import encode,\ + parse_resource_string_from_json, parse_resource_integer_from_json,\ + parse_resource_bytes_from_json, parse_match_operations_from_json,\ + parse_action_parameters_from_json, parse_integer_list_from_json + from p4_exception import UserError, InvalidP4InfoError + +# Logger instance +LOGGER = logging.getLogger(__name__) + +# Global P4Runtime context +CONTEXT = Context() + +# Global P4Runtime client +CLIENT = None + +# Constant P4 entities +KEY_TABLE = "table" +KEY_ACTION = "action" +KEY_ACTION_PROFILE = "action_profile" +KEY_COUNTER = "counter" +KEY_DIR_COUNTER = "direct_counter" +KEY_METER = "meter" +KEY_DIR_METER = "direct_meter" +KEY_CTL_PKT_METADATA = "controller_packet_metadata" + + +def get_context(): + """ + Return P4 context. + + :return: context object + """ + return CONTEXT + + +def get_client(): + """ + Return P4 client. + + :return: P4Runtime client object + """ + return CLIENT + + +def get_api_version(): + """ + Get the supported P4Runtime API version. + + :return: API version + """ + return CLIENT.api_version() + + +def get_table_type(table): + """ + Assess the type of P4 table based upon the matching scheme. + + :param table: P4 table + :return: P4 table type + """ + for m_f in table.match_fields: + if m_f.match_type == p4info_pb2.MatchField.EXACT: + return p4info_pb2.MatchField.EXACT + if m_f.match_type == p4info_pb2.MatchField.LPM: + return p4info_pb2.MatchField.LPM + if m_f.match_type == p4info_pb2.MatchField.TERNARY: + return p4info_pb2.MatchField.TERNARY + if m_f.match_type == p4info_pb2.MatchField.RANGE: + return p4info_pb2.MatchField.RANGE + if m_f.match_type == p4info_pb2.MatchField.OPTIONAL: + return p4info_pb2.MatchField.OPTIONAL + return None + + +def match_type_to_str(match_type): + """ + Convert table match type to string. + + :param match_type: table match type object + :return: table match type string + """ + if match_type == p4info_pb2.MatchField.EXACT: + return "Exact" + if match_type == p4info_pb2.MatchField.LPM: + return "LPM" + if match_type == p4info_pb2.MatchField.TERNARY: + return "Ternary" + if match_type == p4info_pb2.MatchField.RANGE: + return "Range" + if match_type == p4info_pb2.MatchField.OPTIONAL: + return "Optional" + return None + + +def insert_table_entry_exact( + table_name, match_map, action_name, action_params, metadata, + cnt_pkt=-1, cnt_byte=-1): + """ + Insert an entry into an exact match table. + + :param table_name: P4 table name + :param match_map: Map of match operations + :param action_name: Action name + :param action_params: Map of action parameters + :param metadata: table metadata + :param cnt_pkt: packet count + :param cnt_byte: byte count + :return: inserted entry + """ + assert match_map, "Table entry without match operations is not accepted" + assert action_name, "Table entry without action is not accepted" + + table_entry = TableEntry(table_name)(action=action_name) + + for match_k, match_v in match_map.items(): + table_entry.match[match_k] = match_v + + for action_k, action_v in action_params.items(): + table_entry.action[action_k] = action_v + + if metadata: + table_entry.metadata = metadata + + if cnt_pkt > 0: + table_entry.counter_data.packet_count = cnt_pkt + + if cnt_byte > 0: + table_entry.counter_data.byte_count = cnt_byte + + ex_msg = "" + try: + table_entry.insert() + LOGGER.info("Inserted exact table entry: %s", table_entry) + except P4RuntimeWriteException as ex: + ex_msg = str(ex) + except P4RuntimeException as ex: + raise P4RuntimeException from ex + + # Table entry exists, needs to be modified + if "ALREADY_EXISTS" in ex_msg: + table_entry.modify() + LOGGER.info("Updated exact table entry: %s", table_entry) + + return table_entry + + +def insert_table_entry_ternary( + table_name, match_map, action_name, action_params, metadata, + priority, cnt_pkt=-1, cnt_byte=-1): + """ + Insert an entry into a ternary match table. + + :param table_name: P4 table name + :param match_map: Map of match operations + :param action_name: Action name + :param action_params: Map of action parameters + :param metadata: table metadata + :param priority: entry priority + :param cnt_pkt: packet count + :param cnt_byte: byte count + :return: inserted entry + """ + assert match_map, "Table entry without match operations is not accepted" + assert action_name, "Table entry without action is not accepted" + + table_entry = TableEntry(table_name)(action=action_name) + + for match_k, match_v in match_map.items(): + table_entry.match[match_k] = match_v + + for action_k, action_v in action_params.items(): + table_entry.action[action_k] = action_v + + table_entry.priority = priority + + if metadata: + table_entry.metadata = metadata + + if cnt_pkt > 0: + table_entry.counter_data.packet_count = cnt_pkt + + if cnt_byte > 0: + table_entry.counter_data.byte_count = cnt_byte + + ex_msg = "" + try: + table_entry.insert() + LOGGER.info("Inserted ternary table entry: %s", table_entry) + except P4RuntimeWriteException as ex: + ex_msg = str(ex) + except P4RuntimeException as ex: + raise P4RuntimeException from ex + + # Table entry exists, needs to be modified + if "ALREADY_EXISTS" in ex_msg: + table_entry.modify() + LOGGER.info("Updated ternary table entry: %s", table_entry) + + return table_entry + + +def insert_table_entry_range( + table_name, match_map, action_name, action_params, metadata, + priority, cnt_pkt=-1, cnt_byte=-1): # pylint: disable=unused-argument + """ + Insert an entry into a range match table. + + :param table_name: P4 table name + :param match_map: Map of match operations + :param action_name: Action name + :param action_params: Map of action parameters + :param metadata: table metadata + :param priority: entry priority + :param cnt_pkt: packet count + :param cnt_byte: byte count + :return: inserted entry + """ + assert match_map, "Table entry without match operations is not accepted" + assert action_name, "Table entry without action is not accepted" + + raise NotImplementedError( + "Range-based table insertion not implemented yet") + + +def insert_table_entry_optional( + table_name, match_map, action_name, action_params, metadata, + priority, cnt_pkt=-1, cnt_byte=-1): # pylint: disable=unused-argument + """ + Insert an entry into an optional match table. + + :param table_name: P4 table name + :param match_map: Map of match operations + :param action_name: Action name + :param action_params: Map of action parameters + :param metadata: table metadata + :param priority: entry priority + :param cnt_pkt: packet count + :param cnt_byte: byte count + :return: inserted entry + """ + assert match_map, "Table entry without match operations is not accepted" + assert action_name, "Table entry without action is not accepted" + + raise NotImplementedError( + "Optional-based table insertion not implemented yet") + + +class P4Manager: + """ + Class to manage the runtime entries of a P4 pipeline. + """ + + def __init__(self, device_id: int, ip_address: str, port: int, + election_id: tuple, role_name=None, ssl_options=None): + global CLIENT + + self.__id = device_id + self.__ip_address = ip_address + self.__port = int(port) + self.__endpoint = f"{self.__ip_address}:{self.__port}" + CLIENT = P4RuntimeClient( + self.__id, self.__endpoint, election_id, role_name, ssl_options) + self.__p4info = None + + # Internal memory for whitebox management + # | -> P4 entities + self.p4_objects = {} + + # | -> P4 entities + self.table_entries = {} + self.counter_entries = {} + self.direct_counter_entries = {} + self.meter_entries = {} + self.direct_meter_entries = {} + self.multicast_groups = {} + self.clone_session_entries = {} + self.action_profile_members = {} + self.action_profile_groups = {} + + def start(self, p4bin_path, p4info_path): + """ + Start the P4 manager. This involves: + (i) setting the forwarding pipeline of the target switch, + (ii) creating a P4 context object, + (iii) Discovering all the entities of the pipeline, and + (iv) initializing necessary data structures of the manager + + :param p4bin_path: Path to the P4 binary file + :param p4info_path: Path to the P4 info file + :return: void + """ + + if not p4bin_path or not os.path.exists(p4bin_path): + LOGGER.warning("P4 binary file not found") + + if not p4info_path or not os.path.exists(p4info_path): + LOGGER.warning("P4 info file not found") + + # Forwarding pipeline is only set iff both files are present + if p4bin_path and p4info_path: + try: + CLIENT.set_fwd_pipe_config(p4info_path, p4bin_path) + except FileNotFoundError as ex: + LOGGER.critical(ex) + CLIENT.tear_down() + raise FileNotFoundError(ex) from ex + except P4RuntimeException as ex: + LOGGER.critical("Error when setting config") + LOGGER.critical(ex) + CLIENT.tear_down() + raise P4RuntimeException(ex) from ex + except Exception as ex: # pylint: disable=broad-except + LOGGER.critical("Error when setting config") + CLIENT.tear_down() + raise Exception(ex) from ex + + try: + self.__p4info = CLIENT.get_p4info() + except P4RuntimeException as ex: + LOGGER.critical("Error when retrieving P4Info") + LOGGER.critical(ex) + CLIENT.tear_down() + raise P4RuntimeException(ex) from ex + + CONTEXT.set_p4info(self.__p4info) + self.__discover_objects() + self.__init_objects() + LOGGER.info("P4Runtime manager started") + + def stop(self): + """ + Stop the P4 manager. This involves: + (i) tearing the P4Runtime client down and + (ii) cleaning up the manager's internal memory + + :return: void + """ + global CLIENT + + # gRPC client must already be instantiated + assert CLIENT + + # Trigger connection tear down with the P4Runtime server + CLIENT.tear_down() + CLIENT = None + self.__clear() + LOGGER.info("P4Runtime manager stopped") + + def __clear(self): + """ + Reset basic members of the P4 manager. + + :return: void + """ + self.__id = None + self.__ip_address = None + self.__port = None + self.__endpoint = None + self.__clear_state() + + def __clear_state(self): + """ + Reset the manager's internal memory. + + :return: void + """ + self.table_entries.clear() + self.counter_entries.clear() + self.direct_counter_entries.clear() + self.meter_entries.clear() + self.direct_meter_entries.clear() + self.multicast_groups.clear() + self.clone_session_entries.clear() + self.action_profile_members.clear() + self.action_profile_groups.clear() + self.p4_objects.clear() + + def __init_objects(self): + """ + Parse the discovered P4 objects and initialize internal memory for all + the underlying P4 entities. + + :return: void + """ + global KEY_TABLE, KEY_ACTION, KEY_ACTION_PROFILE, \ + KEY_COUNTER, KEY_DIR_COUNTER, \ + KEY_METER, KEY_DIR_METER, \ + KEY_CTL_PKT_METADATA + + KEY_TABLE = P4Type.table.name + KEY_ACTION = P4Type.action.name + KEY_ACTION_PROFILE = P4Type.action_profile.name + KEY_COUNTER = P4Type.counter.name + KEY_DIR_COUNTER = P4Type.direct_counter.name + KEY_METER = P4Type.meter.name + KEY_DIR_METER = P4Type.direct_meter.name + KEY_CTL_PKT_METADATA = P4Type.controller_packet_metadata.name + assert (k for k in [ + KEY_TABLE, KEY_ACTION, KEY_ACTION_PROFILE, + KEY_COUNTER, KEY_DIR_COUNTER, + KEY_METER, KEY_DIR_METER, + KEY_CTL_PKT_METADATA + ]) + + if not self.p4_objects: + LOGGER.warning( + "Cannot initialize internal memory without discovering " + "the pipeline\'s P4 objects") + return + + # Initialize all sorts of entries + if KEY_TABLE in self.p4_objects: + for table in self.p4_objects[KEY_TABLE]: + self.table_entries[table.name] = [] + + if KEY_COUNTER in self.p4_objects: + for cnt in self.p4_objects[KEY_COUNTER]: + self.counter_entries[cnt.name] = [] + + if KEY_DIR_COUNTER in self.p4_objects: + for d_cnt in self.p4_objects[KEY_DIR_COUNTER]: + self.direct_counter_entries[d_cnt.name] = [] + + if KEY_METER in self.p4_objects: + for meter in self.p4_objects[KEY_METER]: + self.meter_entries[meter.name] = [] + + if KEY_DIR_METER in self.p4_objects: + for d_meter in self.p4_objects[KEY_DIR_METER]: + self.direct_meter_entries[d_meter.name] = [] + + if KEY_ACTION_PROFILE in self.p4_objects: + for act_prof in self.p4_objects[KEY_ACTION_PROFILE]: + self.action_profile_members[act_prof.name] = [] + self.action_profile_groups[act_prof.name] = [] + + def __discover_objects(self): + """ + Discover and store all P4 objects. + + :return: void + """ + self.__clear_state() + + for obj_type in P4Type: + for obj in P4Objects(obj_type): + if obj_type.name not in self.p4_objects: + self.p4_objects[obj_type.name] = [] + self.p4_objects[obj_type.name].append(obj) + + def get_table(self, table_name): + """ + Get a P4 table by name. + + :param table_name: P4 table name + :return: P4 table object + """ + if KEY_TABLE not in self.p4_objects: + return None + for table in self.p4_objects[KEY_TABLE]: + if table.name == table_name: + return table + return None + + def get_tables(self): + """ + Get a list of all P4 tables. + + :return: list of P4 tables or empty list + """ + if KEY_TABLE not in self.p4_objects: + return [] + return self.p4_objects[KEY_TABLE] + + def get_action(self, action_name): + """ + Get action by name. + + :param action_name: name of a P4 action + :return: action object or None + """ + if KEY_ACTION not in self.p4_objects: + return None + for action in self.p4_objects[KEY_ACTION]: + if action.name == action_name: + return action + return None + + def get_actions(self): + """ + Get a list of all P4 actions. + + :return: list of P4 actions or empty list + """ + if KEY_ACTION not in self.p4_objects: + return [] + return self.p4_objects[KEY_ACTION] + + def get_action_profile(self, action_prof_name): + """ + Get action profile by name. + + :param action_prof_name: name of the action profile + :return: action profile object or None + """ + if KEY_ACTION_PROFILE not in self.p4_objects: + return None + for action_prof in self.p4_objects[KEY_ACTION_PROFILE]: + if action_prof.name == action_prof_name: + return action_prof + return None + + def get_action_profiles(self): + """ + Get a list of all P4 action profiles. + + :return: list of P4 action profiles or empty list + """ + if KEY_ACTION_PROFILE not in self.p4_objects: + return [] + return self.p4_objects[KEY_ACTION_PROFILE] + + def get_counter(self, cnt_name): + """ + Get counter by name. + + :param cnt_name: name of a P4 counter + :return: counter object or None + """ + if KEY_COUNTER not in self.p4_objects: + return None + for cnt in self.p4_objects[KEY_COUNTER]: + if cnt.name == cnt_name: + return cnt + return None + + def get_counters(self): + """ + Get a list of all P4 counters. + + :return: list of P4 counters or empty list + """ + if KEY_COUNTER not in self.p4_objects: + return [] + return self.p4_objects[KEY_COUNTER] + + def get_direct_counter(self, dir_cnt_name): + """ + Get direct counter by name. + + :param dir_cnt_name: name of a direct P4 counter + :return: direct counter object or None + """ + if KEY_DIR_COUNTER not in self.p4_objects: + return None + for d_cnt in self.p4_objects[KEY_DIR_COUNTER]: + if d_cnt.name == dir_cnt_name: + return d_cnt + return None + + def get_direct_counters(self): + """ + Get a list of all direct P4 counters. + + :return: list of direct P4 counters or empty list + """ + if KEY_DIR_COUNTER not in self.p4_objects: + return [] + return self.p4_objects[KEY_DIR_COUNTER] + + def get_meter(self, meter_name): + """ + Get meter by name. + + :param meter_name: name of a P4 meter + :return: meter object or None + """ + if KEY_METER not in self.p4_objects: + return None + for meter in self.p4_objects[KEY_METER]: + if meter.name == meter_name: + return meter + return None + + def get_meters(self): + """ + Get a list of all P4 meters. + + :return: list of P4 meters or empty list + """ + if KEY_METER not in self.p4_objects: + return [] + return self.p4_objects[KEY_METER] + + def get_direct_meter(self, dir_meter_name): + """ + Get direct meter by name. + + :param dir_meter_name: name of a direct P4 meter + :return: direct meter object or None + """ + if KEY_DIR_METER not in self.p4_objects: + return None + for d_meter in self.p4_objects[KEY_DIR_METER]: + if d_meter.name == dir_meter_name: + return d_meter + return None + + def get_direct_meters(self): + """ + Get a list of all direct P4 meters. + + :return: list of direct P4 meters or empty list + """ + if KEY_DIR_METER not in self.p4_objects: + return [] + return self.p4_objects[KEY_DIR_METER] + + def get_ctl_pkt_metadata(self, ctl_pkt_meta_name): + """ + Get a packet replication object by name. + + :param ctl_pkt_meta_name: name of a P4 packet replication object + :return: P4 packet replication object or None + """ + if KEY_CTL_PKT_METADATA not in self.p4_objects: + return None + for pkt_meta in self.p4_objects[KEY_CTL_PKT_METADATA]: + if ctl_pkt_meta_name == pkt_meta.name: + return pkt_meta + return None + + def get_resource_keys(self): + """ + Retrieve the available P4 resource keys. + + :return: list of P4 resource keys + """ + return list(self.p4_objects.keys()) + + def count_active_entries(self): + """ + Count the number of active entries across all supported P4 entities. + + :return: active number of entries + """ + tot_cnt = \ + self.count_table_entries_all() + \ + self.count_counter_entries_all() + \ + self.count_direct_counter_entries_all() + \ + self.count_meter_entries_all() + \ + self.count_direct_meter_entries_all() + \ + self.count_action_prof_member_entries_all() + \ + self.count_action_prof_group_entries_all() + + return tot_cnt + + ############################################################################ + # Table methods + ############################################################################ + def get_table_names(self): + """ + Retrieve a list of P4 table names. + + :return: list of P4 table names + """ + if KEY_TABLE not in self.p4_objects: + return [] + return list(table.name for table in self.p4_objects[KEY_TABLE]) + + def get_table_entries(self, table_name, action_name=None): + """ + Get a list of P4 table entries by table name and optionally by action. + + :param table_name: name of a P4 table + :param action_name: action name + :return: list of P4 table entries or None + """ + if table_name not in self.table_entries: + return None + self.table_entries[table_name].clear() + self.table_entries[table_name] = [] + + try: + for count, table_entry in enumerate( + TableEntry(table_name)(action=action_name).read()): + LOGGER.debug( + "Table %s - Entry %d\n%s", table_name, count, table_entry) + self.table_entries[table_name].append(table_entry) + return self.table_entries[table_name] + except P4RuntimeException as ex: + LOGGER.error(ex) + return [] + + def table_entries_to_json(self, table_name): + """ + Encode all entries of a P4 table into a JSON object. + + :param table_name: name of a P4 table + :return: JSON object with table entries + """ + if (KEY_TABLE not in self.p4_objects) or \ + not self.p4_objects[KEY_TABLE]: + LOGGER.warning("No table entries to retrieve\n") + return {} + + table_res = {} + + for table in self.p4_objects[KEY_TABLE]: + if not table.name == table_name: + continue + + entries = self.get_table_entries(table.name) + if len(entries) == 0: + continue + + table_res["table-name"] = table_name + + for ent in entries: + entry_match_field = "\n".join(ent.match.fields()) + entry_match_type = match_type_to_str( + ent.match.match_type(entry_match_field)) + + table_res["id"] = ent.id + table_res["match-fields"] = [] + for match_field in ent.match.fields(): + table_res["match-fields"].append( + { + "match-field": match_field, + "match-value": ent.match.value(match_field), + "match-type": entry_match_type + } + ) + table_res["actions"] = [] + table_res["actions"].append( + { + "action-id": ent.action.id(), + "action": ent.action.alias() + } + ) + table_res["priority"] = ent.priority + table_res["is-default"] = ent.is_default + table_res["idle-timeout"] = ent.idle_timeout_ns + if ent.metadata: + table_res["metadata"] = ent.metadata + + return table_res + + def count_table_entries(self, table_name, action_name=None): + """ + Count the number of entries in a P4 table. + + :param table_name: name of a P4 table + :param action_name: action name + :return: number of P4 table entries or negative integer + upon missing table + """ + entries = self.get_table_entries(table_name, action_name) + if entries is None: + return -1 + return len(entries) + + def count_table_entries_all(self): + """ + Count all entries in a P4 table. + + :return: number of P4 table entries + """ + total_cnt = 0 + for table_name in self.get_table_names(): + cnt = self.count_table_entries(table_name) + if cnt < 0: + continue + total_cnt += cnt + return total_cnt + + def table_entry_operation_from_json( + self, json_resource, operation: WriteOperation): + """ + Parse a JSON-based table entry and insert/update/delete it + into/from the switch. + + :param json_resource: JSON-based table entry + :param operation: Write operation (i.e., insert, modify, delete) + to perform. + :return: inserted entry or None in case of parsing error + """ + + table_name = parse_resource_string_from_json( + json_resource, "table-name") + match_map = parse_match_operations_from_json(json_resource) + action_name = parse_resource_string_from_json( + json_resource, "action-name") + action_params = parse_action_parameters_from_json(json_resource) + priority = parse_resource_integer_from_json(json_resource, "priority") + metadata = parse_resource_bytes_from_json(json_resource, "metadata") + + if operation in [WriteOperation.insert, WriteOperation.update]: + LOGGER.debug("Table entry to insert/update: %s", json_resource) + return self.insert_table_entry( + table_name=table_name, + match_map=match_map, + action_name=action_name, + action_params=action_params, + priority=priority, + metadata=metadata if metadata else None + ) + if operation == WriteOperation.delete: + LOGGER.debug("Table entry to delete: %s", json_resource) + return self.delete_table_entry( + table_name=table_name, + match_map=match_map, + action_name=action_name, + action_params=action_params, + priority=priority + ) + return None + + def insert_table_entry(self, table_name, + match_map, action_name, action_params, + priority, metadata=None, cnt_pkt=-1, cnt_byte=-1): + """ + Insert an entry into a P4 table. + This method has internal logic to discriminate among: + (i) Exact matches, + (ii) Ternary matches, + (iii) LPM matches, + (iv) Range matches, and + (v) Optional matches + + :param table_name: name of a P4 table + :param match_map: map of match operations + :param action_name: action name + :param action_params: map of action parameters + :param priority: entry priority + :param metadata: entry metadata + :param cnt_pkt: packet count + :param cnt_byte: byte count + :return: inserted entry + """ + table = self.get_table(table_name) + assert table, \ + "P4 pipeline does not implement table " + table_name + + if not get_table_type(table): + msg = f"Table {table_name} is undefined, cannot insert entry" + LOGGER.error(msg) + raise UserError(msg) + + # Exact match is supported + if get_table_type(table) == p4info_pb2.MatchField.EXACT: + if priority != 0: + msg = f"Table {table_name} is non-ternary, priority must be 0" + LOGGER.error(msg) + raise UserError(msg) + return insert_table_entry_exact( + table_name, match_map, action_name, action_params, metadata, + cnt_pkt, cnt_byte) + + # Ternary and LPM matches are supported + if get_table_type(table) in \ + [p4info_pb2.MatchField.TERNARY, p4info_pb2.MatchField.LPM]: + if priority == 0: + msg = f"Table {table_name} is ternary, priority must be != 0" + LOGGER.error(msg) + raise UserError(msg) + return insert_table_entry_ternary( + table_name, match_map, action_name, action_params, metadata, + priority, cnt_pkt, cnt_byte) + + # TODO: Cover RANGE match # pylint: disable=W0511 + if get_table_type(table) == p4info_pb2.MatchField.RANGE: + return insert_table_entry_range( + table_name, match_map, action_name, action_params, metadata, + priority, cnt_pkt, cnt_byte) + + # TODO: Cover OPTIONAL match # pylint: disable=W0511 + if get_table_type(table) == p4info_pb2.MatchField.OPTIONAL: + return insert_table_entry_optional( + table_name, match_map, action_name, action_params, metadata, + priority, cnt_pkt, cnt_byte) + + return None + + def delete_table_entry(self, table_name, + match_map, action_name, action_params, priority=0): + """ + Delete an entry from a P4 table. + + :param table_name: name of a P4 table + :param match_map: map of match operations + :param action_name: action name + :param action_params: map of action parameters + :param priority: entry priority + :return: deleted entry + """ + table = self.get_table(table_name) + assert table, \ + "P4 pipeline does not implement table " + table_name + + if not get_table_type(table): + msg = f"Table {table_name} is undefined, cannot delete entry" + LOGGER.error(msg) + raise UserError(msg) + + table_entry = TableEntry(table_name)(action=action_name) + + for match_k, match_v in match_map.items(): + table_entry.match[match_k] = match_v + + for action_k, action_v in action_params.items(): + table_entry.action[action_k] = action_v + + if get_table_type(table) == p4info_pb2.MatchField.EXACT: + if priority != 0: + msg = f"Table {table_name} is non-ternary, priority must be 0" + LOGGER.error(msg) + raise UserError(msg) + + if get_table_type(table) in \ + [p4info_pb2.MatchField.TERNARY, p4info_pb2.MatchField.LPM]: + if priority == 0: + msg = f"Table {table_name} is ternary, priority must be != 0" + LOGGER.error(msg) + raise UserError(msg) + + # TODO: Ensure correctness of RANGE & OPTIONAL # pylint: disable=W0511 + if get_table_type(table) in \ + [p4info_pb2.MatchField.RANGE, p4info_pb2.MatchField.OPTIONAL]: + raise NotImplementedError( + "Range and optional-based table deletion not implemented yet") + + table_entry.priority = priority + + table_entry.delete() + LOGGER.info("Deleted entry %s from table: %s", table_entry, table_name) + + return table_entry + + def delete_table_entries(self, table_name): + """ + Delete all entries of a P4 table. + + :param table_name: name of a P4 table + :return: void + """ + table = self.get_table(table_name) + assert table, \ + "P4 pipeline does not implement table " + table_name + + if not get_table_type(table): + msg = f"Table {table_name} is undefined, cannot delete entry" + LOGGER.error(msg) + raise UserError(msg) + + TableEntry(table_name).read(function=lambda x: x.delete()) + LOGGER.info("Deleted all entries from table: %s", table_name) + + def print_table_entries_spec(self, table_name): + """ + Print the specification of a P4 table. + Specification covers: + (i) match id, + (ii) match field name (e.g., ip_proto), + (iii) match type (e.g., exact, ternary, etc.), + (iv) match bitwidth + (v) action id, and + (vi) action name + + :param table_name: name of a P4 table + :return: void + """ + if (KEY_TABLE not in self.p4_objects) or \ + not self.p4_objects[KEY_TABLE]: + LOGGER.warning("No table specification to print\n") + return + + for table in self.p4_objects[KEY_TABLE]: + if not table.name == table_name: + continue + + entry = [] + + for i, match_field in enumerate(table.match_fields): + table_name = table.name if i == 0 else "" + match_field_id = match_field.id + match_field_name = match_field.name + match_type_str = match_type_to_str(match_field.match_type) + match_field_bitwidth = match_field.bitwidth + + entry.append( + [ + table_name, str(match_field_id), match_field_name, + match_type_str, str(match_field_bitwidth) + ] + ) + + print( + tabulate( + entry, + headers=[ + KEY_TABLE, "match id", "match field", + "match type", "match width" + ], + stralign="right", + tablefmt="pretty" + ) + ) + + entry.clear() + + for i, action in enumerate(table.action_refs): + table_name = table.name if i == 0 else "" + action_id = action.id + action_name = CONTEXT.get_name_from_id(action.id) + entry.append([table_name, str(action_id), action_name]) + + print( + tabulate( + entry, + headers=[KEY_TABLE, "action id", "action name"], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + entry.clear() + + def print_table_entries_summary(self): + """ + Print a summary of a P4 table state. + Summary covers: + (i) table name, + (ii) number of entries in the table, and + (iii) a string of \n-separated entry IDs. + + :return: void + """ + if (KEY_TABLE not in self.p4_objects) or \ + not self.p4_objects[KEY_TABLE]: + LOGGER.warning("No tables to print\n") + return + + entry = [] + + for table in self.p4_objects[KEY_TABLE]: + table_name = table.name + entries = self.get_table_entries(table_name) + entries_nb = len(entries) + entry_ids_str = "\n".join(str(e.id) for e in entries) \ + if entries_nb > 0 else "-" + + entry.append([table_name, entries_nb, entry_ids_str]) + + print( + tabulate( + entry, + headers=[KEY_TABLE, "# of entries", "entry ids"], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + def print_table_entries(self, table_name): + """ + Print all entries of a P4 table. + + :param table_name: name of a P4 table + :return: void + """ + if (KEY_TABLE not in self.p4_objects) or \ + not self.p4_objects[KEY_TABLE]: + LOGGER.warning("No table entries to print\n") + return + + for table in self.p4_objects[KEY_TABLE]: + if not table.name == table_name: + continue + + entry = [] + + entries = self.get_table_entries(table.name) + for ent in entries: + entry_id = ent.id + mfs = ent.match.fields() + entry_match_field = "\n".join(mfs) + entry_match_value = "\n".join( + ent.match.value(match_field) for match_field in mfs + ) + entry_match_type = match_type_to_str( + ent.match.match_type(entry_match_field)) + entry_action_id = ent.action.id() + entry_action = ent.action.alias() + entry_priority = ent.priority + entry_is_default = ent.is_default + entry_idle_timeout_ns = ent.idle_timeout_ns + entry_metadata = ent.metadata + + entry.append( + [ + table_name, str(entry_id), + entry_match_field, entry_match_value, entry_match_type, + str(entry_action_id), entry_action, + str(entry_priority), str(entry_is_default), + str(entry_idle_timeout_ns), str(entry_metadata) + ] + ) + + if not entry: + entry.append([table_name] + ["-"] * 10) + + print( + tabulate( + entry, + headers=[ + KEY_TABLE, "table id", + "match field", "match value", "match type", + "action id", "action", "priority", "is default", + "idle timeout (ns)", "metadata" + ], + stralign="right", + tablefmt="pretty", + ) + ) + print("\n") + + ############################################################################ + + ############################################################################ + # Counter methods + ############################################################################ + def get_counter_names(self): + """ + Retrieve a list of P4 counter names. + + :return: list of P4 counter names + """ + if KEY_COUNTER not in self.p4_objects: + return [] + return list(cnt.name for cnt in self.p4_objects[KEY_COUNTER]) + + def get_counter_entries(self, cnt_name): + """ + Get a list of P4 counters by name. + + :param cnt_name: name of a P4 counter + :return: list of P4 counters or None + """ + if cnt_name not in self.counter_entries: + return None + self.counter_entries[cnt_name].clear() + self.counter_entries[cnt_name] = [] + + try: + for count, cnt_entry in enumerate(CounterEntry(cnt_name).read()): + LOGGER.debug( + "Counter %s - Entry %d\n%s", cnt_name, count, cnt_entry) + self.counter_entries[cnt_name].append(cnt_entry) + return self.counter_entries[cnt_name] + except P4RuntimeException as ex: + LOGGER.error(ex) + return [] + + def counter_entries_to_json(self, cnt_name): + """ + Encode all counter entries into a JSON object. + + :param cnt_name: counter name + :return: JSON object with counter entries + """ + if (KEY_COUNTER not in self.p4_objects) or \ + not self.p4_objects[KEY_COUNTER]: + LOGGER.warning("No counter entries to retrieve\n") + return {} + + cnt_res = {} + + for cnt in self.p4_objects[KEY_COUNTER]: + if not cnt.name == cnt_name: + continue + + entries = self.get_counter_entries(cnt.name) + if len(entries) == 0: + continue + + cnt_res["counter-name"] = cnt_name + + for ent in entries: + cnt_res["index"] = ent.index + cnt_res["packet-count"] = ent.packet_count + cnt_res["byte-count"] = ent.byte_count + + return cnt_res + + def count_counter_entries(self, cnt_name): + """ + Count the number of P4 counter entries by counter name. + + :param cnt_name: name of a P4 counter + :return: number of P4 counters or negative integer + upon missing counter + """ + entries = self.get_counter_entries(cnt_name) + if entries is None: + return -1 + return len(entries) + + def count_counter_entries_all(self): + """ + Count all entries of a P4 counter. + + :return: number of P4 counter entries + """ + total_cnt = 0 + for cnt_name in self.get_counter_names(): + cnt = self.count_counter_entries(cnt_name) + if cnt < 0: + continue + total_cnt += cnt + return total_cnt + + def counter_entry_operation_from_json(self, + json_resource, + operation: WriteOperation): + """ + Parse a JSON-based counter entry and insert/update/delete it + into/from the switch. + + :param json_resource: JSON-based counter entry + :param operation: Write operation (i.e., insert, modify, delete) + to perform. + :return: inserted entry or None in case of parsing error + """ + cnt_name = parse_resource_string_from_json( + json_resource, "counter-name") + + if operation in [WriteOperation.insert, WriteOperation.update]: + index = parse_resource_integer_from_json( + json_resource, "index") + cnt_pkt = parse_resource_integer_from_json( + json_resource, "packet-count") + cnt_byte = parse_resource_integer_from_json( + json_resource, "byte-count") + + LOGGER.debug("Counter entry to insert/update: %s", json_resource) + return self.insert_counter_entry( + cnt_name=cnt_name, + index=index, + cnt_pkt=cnt_pkt, + cnt_byte=cnt_byte + ) + if operation == WriteOperation.delete: + LOGGER.debug("Counter entry to delete: %s", json_resource) + return self.clear_counter_entry( + cnt_name=cnt_name + ) + return None + + def insert_counter_entry(self, cnt_name, index=None, + cnt_pkt=-1, cnt_byte=-1): + """ + Insert a P4 counter entry. + + :param cnt_name: name of a P4 counter + :param index: counter index + :param cnt_pkt: packet count + :param cnt_byte: byte count + :return: inserted entry + """ + cnt = self.get_counter(cnt_name) + assert cnt, \ + "P4 pipeline does not implement counter " + cnt_name + + cnt_entry = CounterEntry(cnt_name) + + if index: + cnt_entry.index = index + + if cnt_pkt > 0: + cnt_entry.packet_count = cnt_pkt + + if cnt_byte > 0: + cnt_entry.byte_count = cnt_byte + + cnt_entry.modify() + LOGGER.info("Updated counter entry: %s", cnt_entry) + + return cnt_entry + + def clear_counter_entry(self, cnt_name): + """ + Clear the counters of a counter entry by name. + + :param cnt_name: name of a P4 counter + :return: cleared entry + """ + cnt = self.get_counter(cnt_name) + assert cnt, \ + "P4 pipeline does not implement counter " + cnt_name + + cnt_entry = CounterEntry(cnt_name) + cnt_entry.clear_data() + LOGGER.info("Cleared data of counter entry: %s", cnt_entry) + + return cnt_entry + + def print_counter_entries_summary(self): + """ + Print a summary of a P4 counter state. + Summary covers: + (i) counter name, + (ii) number of entries in the table, and + (iii) a string of \n-separated entry IDs. + + :return: void + """ + if (KEY_COUNTER not in self.p4_objects) or \ + not self.p4_objects[KEY_COUNTER]: + LOGGER.warning("No counters to print\n") + return + + entry = [] + + for cnt in self.p4_objects[KEY_COUNTER]: + entries = self.get_counter_entries(cnt.name) + entries_nb = len(entries) + entry_ids_str = ",".join(str(e.id) for e in entries) \ + if entries_nb > 0 else "-" + entry.append([cnt.name, str(entries_nb), entry_ids_str]) + + print( + tabulate( + entry, + headers=[KEY_COUNTER, "# of entries", "entry ids"], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + ############################################################################ + + ############################################################################ + # Direct counter methods + ############################################################################ + def get_direct_counter_names(self): + """ + Retrieve a list of direct P4 counter names. + + :return: list of direct P4 counter names + """ + if KEY_DIR_COUNTER not in self.p4_objects: + return [] + return list(d_cnt.name for d_cnt in self.p4_objects[KEY_DIR_COUNTER]) + + def get_direct_counter_entries(self, d_cnt_name): + """ + Get a list of direct P4 counters by name. + + :param d_cnt_name: name of a direct P4 counter + :return: list of direct P4 counters or None + """ + if d_cnt_name not in self.direct_counter_entries: + return None + self.direct_counter_entries[d_cnt_name].clear() + self.direct_counter_entries[d_cnt_name] = [] + + try: + for count, d_cnt_entry in enumerate( + DirectCounterEntry(d_cnt_name).read()): + LOGGER.debug( + "Direct counter %s - Entry %d\n%s", + d_cnt_name, count, d_cnt_entry) + self.direct_counter_entries[d_cnt_name].append(d_cnt_entry) + return self.direct_counter_entries[d_cnt_name] + except P4RuntimeException as ex: + LOGGER.error("Failed to get direct counter %s entries: %s", + d_cnt_name, str(ex)) + return [] + + def direct_counter_entries_to_json(self, d_cnt_name): + """ + Encode all direct counter entries into a JSON object. + + :param d_cnt_name: direct counter name + :return: JSON object with direct counter entries + """ + if (KEY_DIR_COUNTER not in self.p4_objects) or \ + not self.p4_objects[KEY_DIR_COUNTER]: + LOGGER.warning("No direct counter entries to retrieve\n") + return {} + + d_cnt_res = {} + + for d_cnt in self.p4_objects[KEY_DIR_COUNTER]: + if not d_cnt.name == d_cnt_name: + continue + + entries = self.get_direct_counter_entries(d_cnt.name) + if len(entries) == 0: + continue + + d_cnt_res["direct-counter-name"] = d_cnt_name + + for ent in entries: + d_cnt_res["match-fields"] = [] + for k, v in ent.table_entry.match.items(): + d_cnt_res["match-fields"].append( + { + "match-field": k, + "match-value": v + } + ) + d_cnt_res["priority"] = ent.priority + d_cnt_res["packet-count"] = ent.packet_count + d_cnt_res["byte-count"] = ent.byte_count + + return d_cnt_res + + def count_direct_counter_entries(self, d_cnt_name): + """ + Count the number of direct P4 counter entries by counter name. + + :param d_cnt_name: name of a direct P4 counter + :return: number of direct P4 counters or negative integer + upon missing direct counter + """ + entries = self.get_direct_counter_entries(d_cnt_name) + if entries is None: + return -1 + return len(entries) + + def count_direct_counter_entries_all(self): + """ + Count all entries of a direct P4 counter. + + :return: number of direct P4 counter entries + """ + total_cnt = 0 + for d_cnt_name in self.get_direct_counter_names(): + cnt = self.count_direct_counter_entries(d_cnt_name) + if cnt < 0: + continue + total_cnt += cnt + return total_cnt + + def direct_counter_entry_operation_from_json(self, + json_resource, + operation: WriteOperation): + """ + Parse a JSON-based direct counter entry and insert/update/delete it + into/from the switch. + + :param json_resource: JSON-based direct counter entry + :param operation: Write operation (i.e., insert, modify, delete) + to perform. + :return: inserted entry or None in case of parsing error + """ + d_cnt_name = parse_resource_string_from_json( + json_resource, "direct-counter-name") + + if operation in [WriteOperation.insert, WriteOperation.update]: + match_map = parse_match_operations_from_json(json_resource) + priority = parse_resource_integer_from_json( + json_resource, "priority") + cnt_pkt = parse_resource_integer_from_json( + json_resource, "packet-count") + cnt_byte = parse_resource_integer_from_json( + json_resource, "byte-count") + + LOGGER.debug( + "Direct counter entry to insert/update: %s", json_resource) + return self.insert_direct_counter_entry( + d_cnt_name=d_cnt_name, + match_map=match_map, + priority=priority, + cnt_pkt=cnt_pkt, + cnt_byte=cnt_byte + ) + if operation == WriteOperation.delete: + LOGGER.debug("Direct counter entry to delete: %s", json_resource) + return self.clear_direct_counter_entry( + d_cnt_name=d_cnt_name + ) + return None + + def insert_direct_counter_entry(self, d_cnt_name, match_map, + priority, cnt_pkt=-1, cnt_byte=-1): + """ + Insert a direct P4 counter entry. + + :param d_cnt_name: name of a direct P4 counter + :param match_map: map of match operations + :param priority: entry priority + :param cnt_pkt: packet count + :param cnt_byte: byte count + :return: inserted entry + """ + d_cnt = self.get_direct_counter(d_cnt_name) + assert d_cnt, \ + "P4 pipeline does not implement direct counter " + d_cnt_name + + assert match_map,\ + "Direct counter entry without match operations is not accepted" + + d_cnt_entry = DirectCounterEntry(d_cnt_name) + + for match_k, match_v in match_map.items(): + d_cnt_entry.table_entry.match[match_k] = match_v + + d_cnt_entry.table_entry.priority = priority + + if cnt_pkt > 0: + d_cnt_entry.packet_count = cnt_pkt + + if cnt_byte > 0: + d_cnt_entry.byte_count = cnt_byte + + d_cnt_entry.modify() + LOGGER.info("Updated direct counter entry: %s", d_cnt_entry) + + return d_cnt_entry + + def clear_direct_counter_entry(self, d_cnt_name): + """ + Clear the counters of a direct counter entry by name. + + :param d_cnt_name: name of a direct P4 counter + :return: cleared entry + """ + d_cnt = self.get_direct_counter(d_cnt_name) + assert d_cnt, \ + "P4 pipeline does not implement direct counter " + d_cnt_name + + d_cnt_entry = DirectCounterEntry(d_cnt_name) + d_cnt_entry.clear_data() + LOGGER.info("Cleared direct counter entry: %s", d_cnt_entry) + + return d_cnt_entry + + def print_direct_counter_entries_summary(self): + """ + Print a summary of a direct P4 counter state. + Summary covers: + (i) direct counter name, + (ii) number of entries in the table, and + (iii) a string of \n-separated entry IDs. + + :return: void + """ + if (KEY_DIR_COUNTER not in self.p4_objects) or \ + not self.p4_objects[KEY_DIR_COUNTER]: + LOGGER.warning("No direct counters to print\n") + return + + entry = [] + + for d_cnt in self.p4_objects[KEY_DIR_COUNTER]: + entries = self.get_direct_counter_entries(d_cnt.name) + entries_nb = len(entries) + entry_ids_str = ",".join(str(e.id) for e in entries) \ + if entries_nb > 0 else "-" + entry.append([d_cnt.name, str(entries_nb), entry_ids_str]) + + print( + tabulate( + entry, + headers=[KEY_DIR_COUNTER, "# of entries", "entry ids"], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + ############################################################################ + + ############################################################################ + # Meter methods + ############################################################################ + def get_meter_names(self): + """ + Retrieve a list of P4 meter names. + + :return: list of P4 meter names + """ + if KEY_METER not in self.p4_objects: + return [] + return list(meter.name for meter in self.p4_objects[KEY_METER]) + + def get_meter_entries(self, meter_name): + """ + Get a list of P4 meters by name. + + :param meter_name: name of a P4 meter + :return: list of P4 meters or None + """ + if meter_name not in self.meter_entries: + return None + self.meter_entries[meter_name].clear() + self.meter_entries[meter_name] = [] + + try: + for count, meter_entry in enumerate(MeterEntry(meter_name).read()): + LOGGER.debug( + "Meter %s - Entry %d\n%s", meter_name, count, meter_entry) + self.meter_entries[meter_name].append(meter_entry) + return self.meter_entries[meter_name] + except P4RuntimeException as ex: + LOGGER.error(ex) + return [] + + def meter_entries_to_json(self, meter_name): + """ + Encode all meter entries into a JSON object. + + :param meter_name: meter name + :return: JSON object with meter entries + """ + if (KEY_METER not in self.p4_objects) or \ + not self.p4_objects[KEY_METER]: + LOGGER.warning("No meter entries to retrieve\n") + return {} + + meter_res = {} + + for meter in self.p4_objects[KEY_METER]: + if not meter.name == meter_name: + continue + + entries = self.get_meter_entries(meter.name) + if len(entries) == 0: + continue + + meter_res["meter-name"] = meter_name + + for ent in entries: + meter_res["index"] = ent.index + meter_res["cir"] = ent.cir + meter_res["cburst"] = ent.cburst + meter_res["pir"] = ent.pir + meter_res["pburst"] = ent.pburst + + return meter_res + + def count_meter_entries(self, meter_name): + """ + Count the number of P4 meter entries by meter name. + + :param meter_name: name of a P4 meter + :return: number of P4 meters or negative integer + upon missing meter + """ + entries = self.get_meter_entries(meter_name) + if entries is None: + return -1 + return len(entries) + + def count_meter_entries_all(self): + """ + Count all entries of a P4 meter. + + :return: number of direct P4 meter entries + """ + total_cnt = 0 + for meter_name in self.get_meter_names(): + cnt = self.count_meter_entries(meter_name) + if cnt < 0: + continue + total_cnt += cnt + return total_cnt + + def meter_entry_operation_from_json(self, + json_resource, + operation: WriteOperation): + """ + Parse a JSON-based meter entry and insert/update/delete it + into/from the switch. + + :param json_resource: JSON-based meter entry + :param operation: Write operation (i.e., insert, modify, delete) + to perform. + :return: inserted entry or None in case of parsing error + """ + meter_name = parse_resource_string_from_json( + json_resource, "meter-name") + + if operation in [WriteOperation.insert, WriteOperation.update]: + index = parse_resource_integer_from_json( + json_resource, "index") + cir = parse_resource_integer_from_json( + json_resource, "committed-information-rate") + cburst = parse_resource_integer_from_json( + json_resource, "committed-burst-size") + pir = parse_resource_integer_from_json( + json_resource, "peak-information-rate") + pburst = parse_resource_integer_from_json( + json_resource, "peak-burst-size") + + LOGGER.debug("Meter entry to insert/update: %s", json_resource) + return self.insert_meter_entry( + meter_name=meter_name, + index=index, + cir=cir, + cburst=cburst, + pir=pir, + pburst=pburst + ) + if operation == WriteOperation.delete: + LOGGER.debug("Meter entry to delete: %s", json_resource) + return self.clear_meter_entry( + meter_name=meter_name + ) + return None + + def insert_meter_entry(self, meter_name, index=None, + cir=-1, cburst=-1, pir=-1, pburst=-1): + """ + Insert a P4 meter entry. + + :param meter_name: name of a P4 meter + :param index: P4 meter index + :param cir: meter's committed information rate + :param cburst: meter's committed burst size + :param pir: meter's peak information rate + :param pburst: meter's peak burst size + :return: inserted entry + """ + meter = self.get_meter(meter_name) + assert meter, \ + "P4 pipeline does not implement meter " + meter_name + + meter_entry = MeterEntry(meter_name) + + if index: + meter_entry.index = index + + if cir > 0: + meter_entry.cir = cir + + if cburst > 0: + meter_entry.cburst = cburst + + if pir > 0: + meter_entry.pir = pir + + if pburst > 0: + meter_entry.pburst = pburst + + meter_entry.modify() + LOGGER.info("Updated meter entry: %s", meter_entry) + + return meter_entry + + def clear_meter_entry(self, meter_name): + """ + Clear the rates and sizes of a meter entry by name. + + :param meter_name: name of a P4 meter + :return: cleared entry + """ + meter = self.get_meter(meter_name) + assert meter, \ + "P4 pipeline does not implement meter " + meter_name + + meter_entry = MeterEntry(meter_name) + meter_entry.clear_config() + LOGGER.info("Cleared meter entry: %s", meter_entry) + + return meter_entry + + def print_meter_entries_summary(self): + """ + Print a summary of a P4 meter state. + Summary covers: + (i) meter name, + (ii) number of entries in the table, and + (iii) a string of \n-separated entry IDs. + + :return: void + """ + if (KEY_METER not in self.p4_objects) or \ + not self.p4_objects[KEY_METER]: + LOGGER.warning("No meters to print\n") + return + + entry = [] + + for meter in self.p4_objects[KEY_METER]: + entries = self.get_meter_entries(meter.name) + entries_nb = len(entries) + entry_ids_str = ",".join(str(e.id) for e in entries) \ + if entries_nb > 0 else "-" + entry.append([meter.name, str(entries_nb), entry_ids_str]) + + print( + tabulate( + entry, + headers=[KEY_METER, "# of entries", "entry ids"], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + ############################################################################ + + ############################################################################ + # Direct meter methods + ############################################################################ + def get_direct_meter_names(self): + """ + Retrieve a list of direct P4 meter names. + + :return: list of direct P4 meter names + """ + if KEY_DIR_METER not in self.p4_objects: + return [] + return list(d_meter.name for d_meter in self.p4_objects[KEY_DIR_METER]) + + def get_direct_meter_entries(self, d_meter_name): + """ + Get a list of direct P4 meters by name. + + :param d_meter_name: name of a direct P4 meter + :return: list of direct P4 meters or None + """ + if d_meter_name not in self.direct_meter_entries: + return None + self.direct_meter_entries[d_meter_name].clear() + self.direct_meter_entries[d_meter_name] = [] + + try: + for count, d_meter_entry in enumerate( + MeterEntry(d_meter_name).read()): + LOGGER.debug( + "Direct meter %s - Entry %d\n%s", + d_meter_name, count, d_meter_entry) + self.direct_meter_entries[d_meter_name].append(d_meter_entry) + return self.direct_meter_entries[d_meter_name] + except P4RuntimeException as ex: + LOGGER.error(ex) + return [] + + def direct_meter_entries_to_json(self, d_meter_name): + """ + Encode all direct meter entries into a JSON object. + + :param d_meter_name: direct meter name + :return: JSON object with direct meter entries + """ + if (KEY_DIR_METER not in self.p4_objects) or \ + not self.p4_objects[KEY_DIR_METER]: + LOGGER.warning("No direct meter entries to retrieve\n") + return {} + + d_meter_res = {} + + for d_meter in self.p4_objects[KEY_DIR_METER]: + if not d_meter.name == d_meter_name: + continue + + entries = self.get_direct_meter_entries(d_meter.name) + if len(entries) == 0: + continue + + d_meter_res["direct-meter-name"] = d_meter_name + + for ent in entries: + d_meter_res["match-fields"] = [] + for k, v in ent.table_entry.match.items(): + d_meter_res["match-fields"].append( + { + "match-field": k, + "match-value": v + } + ) + d_meter_res["cir"] = ent.cir + d_meter_res["cburst"] = ent.cburst + d_meter_res["pir"] = ent.pir + d_meter_res["pburst"] = ent.pburst + + return d_meter_res + + def count_direct_meter_entries(self, d_meter_name): + """ + Count the number of direct P4 meter entries by meter name. + + :param d_meter_name: name of a direct P4 meter + :return: number of direct P4 meters or negative integer + upon missing direct meter + """ + entries = self.get_direct_meter_entries(d_meter_name) + if entries is None: + return -1 + return len(entries) + + def count_direct_meter_entries_all(self): + """ + Count all entries of a direct P4 meter. + + :return: number of direct P4 meter entries + """ + total_cnt = 0 + for d_meter_name in self.get_direct_meter_names(): + cnt = self.count_direct_meter_entries(d_meter_name) + if cnt < 0: + continue + total_cnt += cnt + return total_cnt + + def direct_meter_entry_operation_from_json(self, + json_resource, + operation: WriteOperation): + """ + Parse a JSON-based direct meter entry and insert/update/delete it + into/from the switch. + + :param json_resource: JSON-based direct meter entry + :param operation: Write operation (i.e., insert, modify, delete) + to perform. + :return: inserted entry or None in case of parsing error + """ + d_meter_name = parse_resource_string_from_json( + json_resource, "direct-meter-name") + + if operation in [WriteOperation.insert, WriteOperation.update]: + match_map = parse_match_operations_from_json(json_resource) + cir = parse_resource_integer_from_json( + json_resource, "committed-information-rate") + cburst = parse_resource_integer_from_json( + json_resource, "committed-burst-size") + pir = parse_resource_integer_from_json( + json_resource, "peak-information-rate") + pburst = parse_resource_integer_from_json( + json_resource, "peak-burst-size") + + LOGGER.debug( + "Direct meter entry to insert/update: %s", json_resource) + return self.insert_direct_meter_entry( + d_meter_name=d_meter_name, + match_map=match_map, + cir=cir, + cburst=cburst, + pir=pir, + pburst=pburst + ) + if operation == WriteOperation.delete: + LOGGER.debug("Direct meter entry to delete: %s", json_resource) + return self.clear_direct_meter_entry( + d_meter_name=d_meter_name + ) + return None + + def insert_direct_meter_entry(self, d_meter_name, match_map, + cir=-1, cburst=-1, pir=-1, pburst=-1): + """ + Insert a direct P4 meter entry. + + :param d_meter_name: name of a direct P4 meter + :param match_map: map of P4 table match operations + :param cir: meter's committed information rate + :param cburst: meter's committed burst size + :param pir: meter's peak information rate + :param pburst: meter's peak burst size + :return: inserted entry + """ + d_meter = self.get_direct_meter(d_meter_name) + assert d_meter, \ + "P4 pipeline does not implement direct meter " + d_meter_name + + assert match_map,\ + "Direct meter entry without match operations is not accepted" + + d_meter_entry = DirectMeterEntry(d_meter_name) + + for match_k, match_v in match_map.items(): + d_meter_entry.table_entry.match[match_k] = match_v + + if cir > 0: + d_meter_entry.cir = cir + + if cburst > 0: + d_meter_entry.cburst = cburst + + if pir > 0: + d_meter_entry.pir = pir + + if pburst > 0: + d_meter_entry.pburst = pburst + + d_meter_entry.modify() + LOGGER.info("Updated direct meter entry: %s", d_meter_entry) + + return d_meter_entry + + def clear_direct_meter_entry(self, d_meter_name): + """ + Clear the rates and sizes of a direct meter entry by name. + + :param d_meter_name: name of a direct P4 meter + :return: cleared entry + """ + d_meter = self.get_direct_meter(d_meter_name) + assert d_meter, \ + "P4 pipeline does not implement direct meter " + d_meter_name + + d_meter_entry = DirectMeterEntry(d_meter_name) + d_meter_entry.clear_config() + LOGGER.info("Cleared direct meter entry: %s", d_meter_entry) + + return d_meter_entry + + def print_direct_meter_entries_summary(self): + """ + Print a summary of a direct P4 meter state. + Summary covers: + (i) direct meter name, + (ii) number of entries in the table, and + (iii) a string of \n-separated entry IDs. + + :return: void + """ + if (KEY_DIR_METER not in self.p4_objects) or \ + not self.p4_objects[KEY_DIR_METER]: + LOGGER.warning("No direct meters to print\n") + return + + entry = [] + + for d_meter in self.p4_objects[KEY_DIR_METER]: + entries = self.get_direct_meter_entries(d_meter.name) + entries_nb = len(entries) + entry_ids_str = ",".join(str(e.id) for e in entries) \ + if entries_nb > 0 else "-" + entry.append([d_meter.name, str(entries_nb), entry_ids_str]) + + print( + tabulate( + entry, + headers=[KEY_DIR_METER, "# of entries", "entry ids"], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + ############################################################################ + + ############################################################################ + # Action profile member + ############################################################################ + def get_action_profile_names(self): + """ + Retrieve a list of action profile names. + + :return: list of action profile names + """ + if KEY_ACTION_PROFILE not in self.p4_objects: + return [] + return list(ap_name for ap_name in self.p4_objects[KEY_ACTION_PROFILE]) + + def get_action_prof_member_entries(self, ap_name): + """ + Get a list of action profile members by name. + + :param ap_name: name of a P4 action profile + :return: list of P4 action profile members + """ + if ap_name not in self.action_profile_members: + return None + self.action_profile_members[ap_name].clear() + self.action_profile_members[ap_name] = [] + + try: + for count, ap_entry in enumerate( + ActionProfileMember(ap_name).read()): + LOGGER.debug( + "Action profile member %s - Entry %d\n%s", + ap_name, count, ap_entry) + self.action_profile_members[ap_name].append(ap_entry) + return self.action_profile_members[ap_name] + except P4RuntimeException as ex: + LOGGER.error(ex) + return [] + + def action_prof_member_entries_to_json(self, ap_name): + """ + Encode all action profile members into a JSON object. + + :param ap_name: name of a P4 action profile + :return: JSON object with action profile member entries + """ + if (KEY_ACTION_PROFILE not in self.p4_objects) or \ + not self.p4_objects[KEY_ACTION_PROFILE]: + LOGGER.warning("No action profile member entries to retrieve\n") + return {} + + ap_res = {} + + for act_p in self.p4_objects[KEY_ACTION_PROFILE]: + if not act_p.name == ap_name: + continue + + ap_res["action-profile-name"] = ap_name + + entries = self.get_action_prof_member_entries(ap_name) + for ent in entries: + action = ent.action + action_name = CONTEXT.get_name_from_id(action.id) + ap_res["action"] = action_name + ap_res["action-params"] = [] + for k, v in action.items(): + ap_res["action-params"].append( + { + "param": k, + "value": v + } + ) + + ap_res["member-id"] = ent.member_id + + return ap_res + + def count_action_prof_member_entries(self, ap_name): + """ + Count the number of action profile members by name. + + :param ap_name: name of a P4 action profile + :return: number of action profile members or negative integer + upon missing member + """ + entries = self.get_action_prof_member_entries(ap_name) + if entries is None: + return -1 + return len(entries) + + def count_action_prof_member_entries_all(self): + """ + Count all action profile member entries. + + :return: number of action profile member entries + """ + total_cnt = 0 + for ap_name in self.get_action_profile_names(): + cnt = self.count_action_prof_member_entries(ap_name) + if cnt < 0: + continue + total_cnt += cnt + return total_cnt + + def action_prof_member_entry_operation_from_json(self, + json_resource, + operation: WriteOperation): + """ + Parse a JSON-based action profile member entry and insert/update/delete + it into/from the switch. + + :param json_resource: JSON-based action profile member entry + :param operation: Write operation (i.e., insert, modify, delete) + to perform. + :return: inserted entry or None in case of parsing error + """ + ap_name = parse_resource_string_from_json( + json_resource, "action-profile-name") + member_id = parse_resource_integer_from_json(json_resource, "member-id") + action_name = parse_resource_string_from_json( + json_resource, "action-name") + + if operation in [WriteOperation.insert, WriteOperation.update]: + action_params = parse_action_parameters_from_json(json_resource) + + LOGGER.debug( + "Action profile member entry to insert/update: %s", + json_resource) + return self.insert_action_prof_member_entry( + ap_name=ap_name, + member_id=member_id, + action_name=action_name, + action_params=action_params + ) + if operation == WriteOperation.delete: + LOGGER.debug( + "Action profile member entry to delete: %s", json_resource) + return self.delete_action_prof_member_entry( + ap_name=ap_name, + member_id=member_id, + action_name=action_name + ) + return None + + def insert_action_prof_member_entry(self, ap_name, member_id, + action_name, action_params): + """ + Insert a P4 action profile member entry. + + :param ap_name: name of a P4 action profile + :param member_id: action profile member id + :param action_name: P4 action name + :param action_params: map of P4 action parameters + :return: inserted entry + """ + act_p = self.get_action_profile(ap_name) + assert act_p, \ + "P4 pipeline does not implement action profile " + ap_name + + ap_member_entry = ActionProfileMember(ap_name)( + member_id=member_id, action=action_name) + + for action_k, action_v in action_params.items(): + ap_member_entry.action[action_k] = action_v + + ex_msg = "" + try: + ap_member_entry.insert() + LOGGER.info( + "Inserted action profile member entry: %s", ap_member_entry) + except P4RuntimeWriteException as ex: + ex_msg = str(ex) + except P4RuntimeException as ex: + raise P4RuntimeException from ex + + # Entry exists, needs to be modified + if "ALREADY_EXISTS" in ex_msg: + ap_member_entry.modify() + LOGGER.info( + "Updated action profile member entry: %s", ap_member_entry) + + return ap_member_entry + + def delete_action_prof_member_entry(self, ap_name, member_id, action_name): + """ + Delete a P4 action profile member entry. + + :param ap_name: name of a P4 action profile + :param member_id: action profile member id + :param action_name: P4 action name + :return: deleted entry + """ + act_p = self.get_action_profile(ap_name) + assert act_p, \ + "P4 pipeline does not implement action profile " + ap_name + + ap_member_entry = ActionProfileMember(ap_name)( + member_id=member_id, action=action_name) + ap_member_entry.delete() + LOGGER.info("Deleted action profile member entry: %s", ap_member_entry) + + return ap_member_entry + + def print_action_prof_members_summary(self): + """ + Print a summary of a P4 action profile member state. + Summary covers: + (i) action profile member id, + (ii) number of entries in the table, and + (iii) a string of \n-separated entry IDs. + + :return: void + """ + if (KEY_ACTION_PROFILE not in self.p4_objects) or \ + not self.p4_objects[KEY_ACTION_PROFILE]: + LOGGER.warning("No action profile members to print\n") + return + + entry = [] + + for ap_name in self.p4_objects[KEY_ACTION_PROFILE]: + entries = self.get_action_prof_member_entries(ap_name) + entries_nb = len(entries) + entry_ids_str = ",".join(str(e.member_id) for e in entries) \ + if entries_nb > 0 else "-" + entry.append([ap_name, str(entries_nb), entry_ids_str]) + + print( + tabulate( + entry, + headers=["action profile member", "# of entries", "entry ids"], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + def print_action_prof_member_entries(self, ap_name): + """ + Print all entries of a P4 action profile member. + + :param ap_name: name of a P4 action profile + :return: void + """ + if (KEY_ACTION_PROFILE not in self.p4_objects) or \ + not self.p4_objects[KEY_ACTION_PROFILE]: + LOGGER.warning("No action profile member entries to print\n") + return + + for act_p in self.p4_objects[KEY_ACTION_PROFILE]: + if not act_p.name == ap_name: + continue + + entry = [] + + entries = self.get_action_prof_member_entries(ap_name) + for ent in entries: + member_id = ent.member_id + action = ent.action + action_name = CONTEXT.get_name_from_id(action.id) + + entry.append([ap_name, str(member_id), action_name]) + + if not entry: + entry.append([ap_name] + ["-"] * 2) + + print( + tabulate( + entry, + headers=["action profile member", "member id", "action"], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + ############################################################################ + # Action profile group + ############################################################################ + def get_action_prof_group_entries(self, ap_name): + """ + Get a list of action profile groups by name. + + :param ap_name: name of a P4 action profile + :return: list of P4 action profile groups + """ + if ap_name not in self.action_profile_groups: + return None + self.action_profile_groups[ap_name].clear() + self.action_profile_groups[ap_name] = [] + + try: + for count, ap_entry in enumerate( + ActionProfileGroup(ap_name).read()): + LOGGER.debug("Action profile group %s - Entry %d\n%s", + ap_name, count, ap_entry) + self.action_profile_groups[ap_name].append(ap_entry) + return self.action_profile_groups[ap_name] + except P4RuntimeException as ex: + LOGGER.error(ex) + return [] + + def count_action_prof_group_entries(self, ap_name): + """ + Count the number of action profile groups by name. + + :param ap_name: name of a P4 action profile + :return: number of action profile groups or negative integer + upon missing group + """ + entries = self.get_action_prof_group_entries(ap_name) + if entries is None: + return -1 + return len(entries) + + def count_action_prof_group_entries_all(self): + """ + Count all action profile group entries. + + :return: number of action profile group entries + """ + total_cnt = 0 + for ap_name in self.get_action_profile_names(): + cnt = self.count_action_prof_group_entries(ap_name) + if cnt < 0: + continue + total_cnt += cnt + return total_cnt + + def action_prof_group_entries_to_json(self, ap_name): + """ + Encode all action profile groups into a JSON object. + + :param ap_name: name of a P4 action profile + :return: JSON object with action profile group entries + """ + if (KEY_ACTION_PROFILE not in self.p4_objects) or \ + not self.p4_objects[KEY_ACTION_PROFILE]: + LOGGER.warning("No action profile group entries to retrieve\n") + return {} + + ap_res = {} + + for act_p in self.p4_objects[KEY_ACTION_PROFILE]: + if not act_p.name == ap_name: + continue + + ap_res["action-profile-name"] = ap_name + + entries = self.get_action_prof_group_entries(ap_name) + for ent in entries: + ap_res["group-id"] = ent.group_id + ap_res["members"] = [] + for mem in ent.members: + ap_res["members"].append( + { + "member": mem + } + ) + + return ap_res + + def action_prof_group_entry_operation_from_json(self, + json_resource, + operation: WriteOperation): + """ + Parse a JSON-based action profile group entry and insert/update/delete + it into/from the switch. + + :param json_resource: JSON-based action profile group entry + :param operation: Write operation (i.e., insert, modify, delete) + to perform. + :return: inserted entry or None in case of parsing error + """ + ap_name = parse_resource_string_from_json( + json_resource, "action-profile-name") + group_id = parse_resource_integer_from_json(json_resource, "group-id") + + if operation in [WriteOperation.insert, WriteOperation.update]: + members = parse_integer_list_from_json( + json_resource, "members", "member") + + LOGGER.debug( + "Action profile group entry to insert/update: %s", + json_resource) + return self.insert_action_prof_group_entry( + ap_name=ap_name, + group_id=group_id, + members=members + ) + if operation == WriteOperation.delete: + LOGGER.debug( + "Action profile group entry to delete: %s", json_resource) + return self.delete_action_prof_group_entry( + ap_name=ap_name, + group_id=group_id + ) + return None + + def insert_action_prof_group_entry(self, ap_name, group_id, members=None): + """ + Insert a P4 action profile group entry. + + :param ap_name: name of a P4 action profile + :param group_id: action profile group id + :param members: list of associated action profile members + :return: inserted entry + """ + ap = self.get_action_profile(ap_name) + assert ap, \ + "P4 pipeline does not implement action profile " + ap_name + + ap_group_entry = ActionProfileGroup(ap_name)(group_id=group_id) + + if members: + for m in members: + ap_group_entry.add(member_id=m) + + ex_msg = "" + try: + ap_group_entry.insert() + LOGGER.info( + "Inserted action profile group entry: %s", ap_group_entry) + except P4RuntimeWriteException as ex: + ex_msg = str(ex) + except P4RuntimeException as ex: + raise P4RuntimeException from ex + + # Entry exists, needs to be modified + if "ALREADY_EXISTS" in ex_msg: + ap_group_entry.modify() + LOGGER.info( + "Updated action profile group entry: %s", ap_group_entry) + + return ap_group_entry + + def delete_action_prof_group_entry(self, ap_name, group_id): + """ + Delete a P4 action profile group entry. + + :param ap_name: name of a P4 action profile + :param group_id: action profile group id + :return: deleted entry + """ + ap = self.get_action_profile(ap_name) + assert ap, \ + "P4 pipeline does not implement action profile " + ap_name + + ap_group_entry = ActionProfileGroup(ap_name)(group_id=group_id) + ap_group_entry.delete() + LOGGER.info("Deleted action profile group entry: %s", ap_group_entry) + + return ap_group_entry + + def clear_action_prof_group_entry(self, ap_name, group_id): + """ + Clean a P4 action profile group entry. + + :param ap_name: name of a P4 action profile + :param group_id: action profile group id + :return: cleaned entry + """ + ap = self.get_action_profile(ap_name) + assert ap, \ + "P4 pipeline does not implement action profile " + ap_name + + ap_group_entry = ActionProfileGroup(ap_name)(group_id=group_id) + ap_group_entry.clear() + LOGGER.info("Cleared action profile group entry: %s", ap_group_entry) + + return ap_group_entry + + def print_action_prof_groups_summary(self): + """ + Print a summary of a P4 action profile group state. + Summary covers: + (i) action profile group id, + (ii) number of entries in the table, and + (iii) a string of \n-separated entry IDs. + + :return: void + """ + if (KEY_ACTION_PROFILE not in self.p4_objects) or \ + not self.p4_objects[KEY_ACTION_PROFILE]: + LOGGER.warning("No action profile groups to print\n") + return + + entry = [] + + for ap_name in self.p4_objects[KEY_ACTION_PROFILE]: + entries = self.get_action_prof_group_entries(ap_name) + entries_nb = len(entries) + entry_ids_str = ",".join(str(e.group_id) for e in entries) \ + if entries_nb > 0 else "-" + entry.append([ap_name, str(entries_nb), entry_ids_str]) + + print( + tabulate( + entry, + headers=["action profile group", "# of entries", "entry ids"], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + def print_action_prof_group_entries(self, ap_name): + """ + Print all entries of a P4 action profile group. + + :param ap_name: name of a P4 action profile + :return: void + """ + if (KEY_ACTION_PROFILE not in self.p4_objects) or \ + not self.p4_objects[KEY_ACTION_PROFILE]: + LOGGER.warning("No action profile group entries to print\n") + return + + for ap in self.p4_objects[KEY_ACTION_PROFILE]: + if not ap.name == ap_name: + continue + + entry = [] + + entries = self.get_action_prof_group_entries(ap_name) + for e in entries: + group_id = e.group_id + members_str = "\n".join(m for m in e.members) + entry.append([ap_name, str(group_id), members_str]) + + if not entry: + entry.append([ap_name] + ["-"] * 2) + + print( + tabulate( + entry, + headers=[ + "action profile group", "group id", "members" + ], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + ############################################################################ + # Packet replication method 1: Multicast group + ############################################################################ + def get_multicast_group_entry(self, group_id): + """ + Get a multicast group entry by group id. + + :param group_id: id of a multicast group + :return: multicast group entry or none + """ + if group_id not in self.multicast_groups: + return None + self.multicast_groups[group_id] = None + + try: + mcast_group = MulticastGroupEntry(group_id).read() + LOGGER.debug("Multicast group %d\n%s", group_id, mcast_group) + self.multicast_groups[group_id] = mcast_group + return self.multicast_groups[group_id] + except P4RuntimeException as ex: + LOGGER.error(ex) + return None + + def count_multicast_groups(self): + """ + Count the number of multicast groups. + + :return: number of multicast groups + """ + return len(self.multicast_groups.keys()) + + def multicast_group_entries_to_json(self): + """ + Encode all multicast groups into a JSON object. + + :return: JSON object with multicast group entries + """ + if not self.multicast_groups: + LOGGER.warning("No multicast group entries to retrieve\n") + return {} + + mcast_list_res = [] + + for mcast_group in self.multicast_groups.values(): + mcast_res = {} + mcast_res["group-id"] = mcast_group.group_id + + mcast_res["egress-ports"] = [] + mcast_res["instances"] = [] + for r in mcast_group.replicas: + mcast_res["egress-ports"].append( + { + "egress-port": r.egress_port + } + ) + mcast_res["instances"].append( + { + "instance": r.instance + } + ) + mcast_list_res.append(mcast_res) + + return mcast_list_res + + def multicast_group_entry_operation_from_json(self, + json_resource, + operation: WriteOperation): + """ + Parse a JSON-based multicast group entry and insert/update/delete it + into/from the switch. + + :param json_resource: JSON-based multicast group entry + :param operation: Write operation (i.e., insert, modify, delete) + to perform. + :return: inserted entry or None in case of parsing error + """ + group_id = parse_resource_integer_from_json(json_resource, "group-id") + + if operation in [WriteOperation.insert, WriteOperation.update]: + ports = parse_integer_list_from_json( + json_resource, "ports", "port") + + LOGGER.debug( + "Multicast group entry to insert/update: %s", json_resource) + return self.insert_multicast_group_entry( + group_id=group_id, + ports=ports + ) + if operation == WriteOperation.delete: + LOGGER.debug("Multicast group entry to delete: %s", json_resource) + return self.delete_multicast_group_entry( + group_id=group_id + ) + return None + + def insert_multicast_group_entry(self, group_id, ports): + """ + Insert a new multicast group. + + :param group_id: id of a multicast group + :param ports: list of egress ports to multicast + :return: inserted multicast group + """ + assert group_id > 0, \ + "Multicast group " + group_id + " must be > 0" + assert ports, \ + "No multicast group ports are provided" + + mcast_group = MulticastGroupEntry(group_id) + for p in ports: + mcast_group.add(p, 1) + + ex_msg = "" + try: + mcast_group.insert() + LOGGER.info("Inserted multicast group entry: %s", mcast_group) + except P4RuntimeWriteException as ex: + ex_msg = str(ex) + except P4RuntimeException as ex: + raise P4RuntimeException from ex + + # Entry exists, needs to be modified + if "ALREADY_EXISTS" in ex_msg: + mcast_group.modify() + LOGGER.info("Updated multicast group entry: %s", mcast_group) + + self.multicast_groups[group_id] = mcast_group + + return mcast_group + + def delete_multicast_group_entry(self, group_id): + """ + Delete a multicast group by id. + + :param group_id: id of a multicast group + :return: deleted multicast group + """ + assert group_id > 0, \ + "Multicast group " + group_id + " must be > 0" + + mcast_group = MulticastGroupEntry(group_id) + mcast_group.delete() + + if group_id in self.multicast_groups: + del self.multicast_groups[group_id] + LOGGER.info( + "Deleted multicast group %d", group_id) + + return mcast_group + + def delete_multicast_group_entries(self): + """ + Delete all multicast groups. + + :return: void + """ + for mcast_group in MulticastGroupEntry().read(): + gid = mcast_group.group_id + mcast_group.delete() + del self.multicast_groups[gid] + + assert self.count_multicast_groups() == 0, \ + "Failed to purge all multicast groups" + LOGGER.info("Deleted all multicast groups") + + def print_multicast_groups_summary(self): + """ + Print a summary of a P4 multicast group state. + Summary covers: + (i) multicast group id, + (ii) a string of \n-separated egress ports, and + (iii) a string of \n-separated replica instances. + + :return: void + """ + entry = [] + + for mcast_group in self.multicast_groups.values(): + ports_str = "\n".join( + str(r.egress_port) for r in mcast_group.replicas) + inst_str = "\n".join( + str(r.instance) for r in mcast_group.replicas) + entry.append([str(mcast_group.group_id), ports_str, inst_str]) + + if not entry: + entry.append(3 * ["-"]) + + print( + tabulate( + entry, + headers=["multicast group id", "egress ports", "instances"], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + ############################################################################ + # Packet replication method 2: Clone session + ############################################################################ + def get_clone_session_entry(self, session_id): + """ + Get a clone session entry by session id. + + :param session_id: id of a clone session + :return: clone session entry or none + """ + if session_id not in self.clone_session_entries: + return None + self.clone_session_entries[session_id] = None + + try: + session = CloneSessionEntry(session_id).read() + LOGGER.debug("Clone session %d\n%s", session_id, session) + self.clone_session_entries[session_id] = session + return self.clone_session_entries[session_id] + except P4RuntimeException as ex: + LOGGER.error(ex) + return None + + def count_clone_session_entries(self): + """ + Count the number of clone sessions. + + :return: number of clone sessions + """ + return len(self.clone_session_entries.keys()) + + def clone_session_entries_to_json(self): + """ + Encode all clone sessions into a JSON object. + + :return: JSON object with clone session entries + """ + if not self.clone_session_entries: + LOGGER.warning("No clone session entries to retrieve\n") + return {} + + session_list_res = [] + + for session in self.clone_session_entries.values(): + session_res = {} + session_res["session-id"] = session.session_id + + session_res["egress-ports"] = [] + session_res["instances"] = [] + for r in session.replicas: + session_res["egress-ports"].append( + { + "egress-port": r.egress_port + } + ) + session_res["instances"].append( + { + "instance": r.instance + } + ) + session_list_res.append(session_res) + + return session_list_res + + def clone_session_entry_operation_from_json(self, + json_resource, + operation: WriteOperation): + """ + Parse a JSON-based clone session entry and insert/update/delete it + into/from the switch. + + :param json_resource: JSON-based clone session entry + :param operation: Write operation (i.e., insert, modify, delete) + to perform. + :return: inserted entry or None in case of parsing error + """ + session_id = parse_resource_integer_from_json( + json_resource, "session-id") + + if operation in [WriteOperation.insert, WriteOperation.update]: + ports = parse_integer_list_from_json( + json_resource, "ports", "port") + + LOGGER.debug( + "Clone session entry to insert/update: %s", json_resource) + return self.insert_clone_session_entry( + session_id=session_id, + ports=ports + ) + if operation == WriteOperation.delete: + LOGGER.debug( + "Clone session entry to delete: %s", json_resource) + return self.delete_clone_session_entry( + session_id=session_id + ) + return None + + def insert_clone_session_entry(self, session_id, ports): + """ + Insert a new clone session. + + :param session_id: id of a clone session + :param ports: list of egress ports to clone session + :return: inserted clone session + """ + assert session_id > 0, \ + "Clone session " + session_id + " must be > 0" + assert ports, \ + "No clone session ports are provided" + + session = CloneSessionEntry(session_id) + for p in ports: + session.add(p, 1) + + ex_msg = "" + try: + session.insert() + LOGGER.info("Inserted clone session entry: %s", session) + except P4RuntimeWriteException as ex: + ex_msg = str(ex) + except P4RuntimeException as ex: + raise P4RuntimeException from ex + + # Entry exists, needs to be modified + if "ALREADY_EXISTS" in ex_msg: + session.modify() + LOGGER.info("Updated clone session entry: %s", session) + + self.clone_session_entries[session_id] = session + + return session + + def delete_clone_session_entry(self, session_id): + """ + Delete a clone session by id. + + :param session_id: id of a clone session + :return: deleted clone session + """ + assert session_id > 0, \ + "Clone session " + session_id + " must be > 0" + + session = CloneSessionEntry(session_id) + session.delete() + + if session_id in self.clone_session_entries: + del self.clone_session_entries[session_id] + LOGGER.info( + "Deleted clone session %d", session_id) + + return session + + def delete_clone_session_entries(self): + """ + Delete all clone sessions. + + :return: void + """ + for e in CloneSessionEntry().read(): + sid = e.session_id + e.delete() + del self.clone_session_entries[sid] + + assert self.count_multicast_groups() == 0, \ + "Failed to purge all clone sessions" + LOGGER.info("Deleted all clone sessions") + + def print_clone_sessions_summary(self): + """ + Print a summary of a P4 clone session state. + Summary covers: + (i) clone session id, + (ii) a string of \n-separated egress ports, and + (iii) a string of \n-separated replica instances. + + :return: void + """ + entry = [] + + for session in self.clone_session_entries.values(): + ports_str = "\n".join( + str(r.egress_port) for r in session.replicas) + inst_str = "\n".join( + str(r.instance) for r in session.replicas) + entry.append([str(session.session_id), ports_str, inst_str]) + + if not entry: + entry.append(3 * ["-"]) + + print( + tabulate( + entry, + headers=["clone session id", "egress ports", "instances"], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + ############################################################################ + # Packet replication method 3: Packet in + ############################################################################ + def get_packet_metadata(self, meta_type, attr_name=None, attr_id=None): + """ + Retrieve the pipeline's metadata by metadata type field. + + :param meta_type: metadata type field + :param attr_name: metadata name field (optional) + :param attr_id: metadata id field (optional) + :return: packet metadata + """ + for table in self.__p4info.controller_packet_metadata: + pre = table.preamble + if pre.name == meta_type: + for meta in table.metadata: + if attr_name is not None: + if meta.name == attr_name: + return meta + elif attr_id is not None: + if meta.id == attr_id: + return meta + raise AttributeError( + f"ControllerPacketMetadata {meta_type} has no metadata " + f"{attr_name if attr_name is not None else attr_id} (check P4Info)") + + # TODO: test packet in # pylint: disable=W0511 + def create_packet_in(self, payload, metadata=None): + """ + Create a packet-in object. + + :param payload: packet-in payload + :param metadata: packet-in metadata (optional) + :return: packet-in object + """ + if not self.p4_objects[KEY_CTL_PKT_METADATA]: + LOGGER.warning("Cannot create packet in. " + "No controller packet metadata in the pipeline\n") + return None + + packet_in = PacketOut() + packet_in.payload = payload + if metadata: + for name, value in metadata.items(): + p4info_meta = self.get_packet_metadata("packet_in", name) + meta = packet_in.metadata.add() + meta.metadata_id = p4info_meta.id + meta.value = encode(value, p4info_meta.bitwidth) + return packet_in + + def send_packet_in(self, payload, metadata=None, timeout=1): + """ + Send a packet-in message. + Note that the sniff method is blocking, thus it should be invoked by + another thread. + + :param payload: packet-in payload + :param metadata: packet-in metadata (optional) + :param timeout: packet-in timeout (defaults to 1s) + :return: void + """ + packet_in = self.create_packet_in(payload, metadata) + + # TODO: experimental piece of code # pylint: disable=W0511 + captured_packet = [] + + def _sniff_packet(captured_pkt): + """ + Invoke packet-in sniff method. + + :param captured_pkt: buffer for the packet to be captured + :return: void + """ + captured_pkt += packet_in.sniff(timeout=timeout) + + _t = Thread(target=_sniff_packet, args=(captured_packet,)) + _t.start() + # P4Runtime client sends the packet to the switch + CLIENT.stream_in_q["packet"].put(packet_in) + _t.join() + LOGGER.info("Packet-in sent: %s", packet_in) + + ############################################################################ + # Packet replication method 4: Packet out + ############################################################################ + # TODO: test packet out # pylint: disable=W0511 + def create_packet_out(self, payload, metadata=None): + """ + Create a packet-out object. + + :param payload: packet-out payload + :param metadata: packet-out metadata (optional) + :return: packet-out object + """ + if not self.p4_objects[KEY_CTL_PKT_METADATA]: + LOGGER.warning("Cannot create packet out. " + "No controller packet metadata in the pipeline\n") + return None + + packet_out = PacketOut() + packet_out.payload = payload + if metadata: + for name, value in metadata.items(): + p4info_meta = self.get_packet_metadata("packet_out", name) + meta = packet_out.metadata.add() + meta.metadata_id = p4info_meta.id + meta.value = encode(value, p4info_meta.bitwidth) + return packet_out + + def send_packet_out(self, payload, metadata=None): + """ + Send a packet-out message. + + :param payload: packet-out payload + :param metadata: packet-out metadata (optional) + :return: void + """ + packet_out = self.create_packet_out(payload, metadata) + packet_out.send() + LOGGER.info("Packet-out sent: %s", packet_out) + + ############################################################################ + # Packet replication method 5: Idle timeout notification + ############################################################################ + # TODO: Support IdleTimeoutNotification # pylint: disable=W0511 + ############################################################################ + + def print_objects(self): + """ + Print all P4 objects of the installed pipeline. + + :return: void + """ + if not self.p4_objects: + self.__discover_objects() + + for obj_name, objects in self.p4_objects.items(): + entry = [] + + for obj in objects: + entry.append([obj.name]) + + if not entry: + entry.append("-") + print( + tabulate( + entry, + headers=[obj_name], + stralign="right", + tablefmt="pretty" + ) + ) + print("\n") + + +class P4Object: + """ + P4 object. + """ + + def __init__(self, obj_type, obj): + self.name = obj.preamble.name + self.id = obj.preamble.id + self._obj_type = obj_type + self._obj = obj + self.__doc__ = f""" +A wrapper around the P4Info Protobuf message for +{obj_type.pretty_name} '{self.name}'. +You can access any field from the message with <self>.<field name>. +You can access the name directly with <self>.name. +You can access the id directly with <self>.id. +If you need the underlying Protobuf message, you can access it with msg(). +""" + + def __getattr__(self, name): + return getattr(self._obj, name) + + def __settattr__(self, name, value): + return UserError( + f"Operation {name}:{value} not supported") + + def msg(self): + """Get Protobuf message object""" + return self._obj + + def actions(self): + """Print list of actions, only for tables and action profiles.""" + if self._obj_type == P4Type.table: + for action in self._obj.action_refs: + print(CONTEXT.get_name_from_id(action.id)) + elif self._obj_type == P4Type.action_profile: + t_id = self._obj.table_ids[0] + t_name = CONTEXT.get_name_from_id(t_id) + t = CONTEXT.get_table(t_name) + for action in t.action_refs: + print(CONTEXT.get_name_from_id(action.id)) + else: + raise UserError( + "'actions' is only available for tables and action profiles") + + +class P4Objects: + """ + P4 objects. + """ + + def __init__(self, obj_type): + self._obj_type = obj_type + self._names = sorted([name for name, _ in CONTEXT.get_objs(obj_type)]) + self._iter = None + self.__doc__ = """ +All the {pnames} in the P4 program. +To access a specific {pname}, use {p4info}['<name>']. +You can use this class to iterate over all {pname} instances: +\tfor x in {p4info}: +\t\tprint(x.id) +""".format(pname=obj_type.pretty_name, pnames=obj_type.pretty_names, + p4info=obj_type.p4info_name) + + def __getitem__(self, name): + obj = CONTEXT.get_obj(self._obj_type, name) + if obj is None: + raise UserError( + f"{self._obj_type.pretty_name} '{name}' does not exist") + return P4Object(self._obj_type, obj) + + def __setitem__(self, name, value): + raise UserError("Operation not allowed") + + def __iter__(self): + self._iter = iter(self._names) + return self + + def __next__(self): + name = next(self._iter) + return self[name] + + +class MatchKey: + """ + P4 match key. + """ + + def __init__(self, table_name, match_fields): + self._table_name = table_name + self._fields = OrderedDict() + self._fields_suffixes = {} + for mf in match_fields: + self._add_field(mf) + self._mk = OrderedDict() + self._set_docstring() + + def _set_docstring(self): + self.__doc__ = f"Match key fields for table '{self._table_name}':\n\n" + for _, info in self._fields.items(): + self.__doc__ += str(info) + self.__doc__ += """ +Set a field value with <self>['<field_name>'] = '...' + * For exact match: <self>['<f>'] = '<value>' + * For ternary match: <self>['<f>'] = '<value>&&&<mask>' + * For LPM match: <self>['<f>'] = '<value>/<mask>' + * For range match: <self>['<f>'] = '<value>..<mask>' + * For optional match: <self>['<f>'] = '<value>' + +If it's inconvenient to use the whole field name, you can use a unique suffix. + +You may also use <self>.set(<f>='<value>') +\t(<f> must not include a '.' in this case, +but remember that you can use a unique suffix) +""" + + def _get_mf(self, name): + if name in self._fields: + return self._fields[name] + if name in self._fields_suffixes: + return self._fields[self._fields_suffixes[name]] + raise UserError( + f"'{name}' is not a valid match field name, nor a valid unique " + f"suffix, for table '{self._table_name}'") + + def __setitem__(self, name, value): + field_info = self._get_mf(name) + self._mk[name] = self._parse_mf(value, field_info) + print(self._mk[name]) + + def __getitem__(self, name): + _ = self._get_mf(name) + print(self._mk.get(name, "Unset")) + + def _parse_mf(self, s, field_info): + if not isinstance(s, str): + raise UserError("Match field value must be a string") + if field_info.match_type == p4info_pb2.MatchField.EXACT: + return self._parse_mf_exact(s, field_info) + if field_info.match_type == p4info_pb2.MatchField.LPM: + return self._parse_mf_lpm(s, field_info) + if field_info.match_type == p4info_pb2.MatchField.TERNARY: + return self._parse_mf_ternary(s, field_info) + if field_info.match_type == p4info_pb2.MatchField.RANGE: + return self._parse_mf_range(s, field_info) + if field_info.match_type == p4info_pb2.MatchField.OPTIONAL: + return self._parse_mf_optional(s, field_info) + raise UserError( + f"Unsupported match type for field:\n{field_info}") + + def _parse_mf_exact(self, s, field_info): + v = encode(s.strip(), field_info.bitwidth) + return self._sanitize_and_convert_mf_exact(v, field_info) + + def _sanitize_and_convert_mf_exact(self, value, field_info): + mf = p4runtime_pb2.FieldMatch() + mf.field_id = field_info.id + mf.exact.value = make_canonical_if_option_set(value) + return mf + + def _parse_mf_optional(self, s, field_info): + v = encode(s.strip(), field_info.bitwidth) + return self._sanitize_and_convert_mf_optional(v, field_info) + + def _sanitize_and_convert_mf_optional(self, value, field_info): + mf = p4runtime_pb2.FieldMatch() + mf.field_id = field_info.id + mf.optional.value = make_canonical_if_option_set(value) + return mf + + def _parse_mf_lpm(self, s, field_info): + try: + prefix, length = s.split('/') + prefix, length = prefix.strip(), length.strip() + except ValueError: + prefix = s + length = str(field_info.bitwidth) + + prefix = encode(prefix, field_info.bitwidth) + try: + length = int(length) + except ValueError as ex: + raise UserError(f"'{length}' is not a valid prefix length") from ex + + return self._sanitize_and_convert_mf_lpm(prefix, length, field_info) + + def _sanitize_and_convert_mf_lpm(self, prefix, length, field_info): + if length == 0: + raise UserError( + "Ignoring LPM don't care match (prefix length of 0) " + "as per P4Runtime spec") + + mf = p4runtime_pb2.FieldMatch() + mf.field_id = field_info.id + mf.lpm.prefix_len = length + + first_byte_masked = length // 8 + if first_byte_masked == len(prefix): + mf.lpm.value = prefix + return mf + + barray = bytearray(prefix) + transformed = False + r = length % 8 + byte_mask = 0xff & ((0xff << (8 - r))) + if barray[first_byte_masked] & byte_mask != barray[first_byte_masked]: + transformed = True + barray[first_byte_masked] = barray[first_byte_masked] & byte_mask + + for i in range(first_byte_masked + 1, len(prefix)): + if barray[i] != 0: + transformed = True + barray[i] = 0 + if transformed: + print("LPM value was transformed to conform to the P4Runtime spec " + "(trailing bits must be unset)") + mf.lpm.value = bytes(make_canonical_if_option_set(barray)) + return mf + + def _parse_mf_ternary(self, s, field_info): + try: + value, mask = s.split('&&&') + value, mask = value.strip(), mask.strip() + except ValueError: + value = s.strip() + mask = "0b" + ("1" * field_info.bitwidth) + + value = encode(value, field_info.bitwidth) + mask = encode(mask, field_info.bitwidth) + + return self._sanitize_and_convert_mf_ternary(value, mask, field_info) + + def _sanitize_and_convert_mf_ternary(self, value, mask, field_info): + if int.from_bytes(mask, byteorder='big') == 0: + raise UserError( + "Ignoring ternary don't care match (mask of 0s) " + "as per P4Runtime spec") + + mf = p4runtime_pb2.FieldMatch() + mf.field_id = field_info.id + + barray = bytearray(value) + transformed = False + for i in range(len(value)): + if barray[i] & mask[i] != barray[i]: + transformed = True + barray[i] = barray[i] & mask[i] + if transformed: + print("Ternary value was transformed to conform to " + "the P4Runtime spec (masked off bits must be unset)") + mf.ternary.value = bytes( + make_canonical_if_option_set(barray)) + mf.ternary.mask = make_canonical_if_option_set(mask) + return mf + + def _parse_mf_range(self, s, field_info): + try: + start, end = s.split('..') + start, end = start.strip(), end.strip() + except ValueError as ex: + raise UserError(f"'{s}' does not specify a valid range, " + f"use '<start>..<end>'") from ex + + start = encode(start, field_info.bitwidth) + end = encode(end, field_info.bitwidth) + + return self._sanitize_and_convert_mf_range(start, end, field_info) + + def _sanitize_and_convert_mf_range(self, start, end, field_info): + start_ = int.from_bytes(start, byteorder='big') + end_ = int.from_bytes(end, byteorder='big') + if start_ > end_: + raise UserError("Invalid range match: start is greater than end") + if start_ == 0 and end_ == ((1 << field_info.bitwidth) - 1): + raise UserError( + "Ignoring range don't care match (all possible values) " + "as per P4Runtime spec") + mf = p4runtime_pb2.FieldMatch() + mf.field_id = field_info.id + mf.range.low = make_canonical_if_option_set(start) + mf.range.high = make_canonical_if_option_set(end) + return mf + + def _add_field(self, field_info): + self._fields[field_info.name] = field_info + self._recompute_suffixes() + + def _recompute_suffixes(self): + suffixes = {} + suffix_count = Counter() + for fname in self._fields: + suffix = None + for s in reversed(fname.split(".")): + suffix = s if suffix is None else s + "." + suffix + suffixes[suffix] = fname + suffix_count[suffix] += 1 + for suffix, c in suffix_count.items(): + if c > 1: + del suffixes[suffix] + self._fields_suffixes = suffixes + + def __str__(self): + return '\n'.join([str(mf) for name, mf in self._mk.items()]) + + def fields(self): + """ + Return a list of match fields. + + :return: list of match fields or None + """ + fields = [] + for name, _ in self._mk.items(): + fields.append(name) + return fields + + def value(self, field_name): + """ + Get the value of a match field. + + :param field_name: match field name + :return: match field value + """ + for name, info in self._fields.items(): + if name != field_name: + continue + if info.match_type == p4info_pb2.MatchField.EXACT: + return self._mk[name].exact.value.hex() + if info.match_type == p4info_pb2.MatchField.LPM: + return self._mk[name].lpm.value.hex() + if info.match_type == p4info_pb2.MatchField.TERNARY: + return self._mk[name].ternary.value.hex() + if info.match_type == p4info_pb2.MatchField.RANGE: + return self._mk[name].range.value.hex() + if info.match_type == p4info_pb2.MatchField.OPTIONAL: + return self._mk[name].optional.value.hex() + return None + + def match_type(self, field_name): + """ + Get the type of a match field. + + :param field_name: match field name + :return: match field type + """ + for name, info in self._fields.items(): + if name not in field_name: + continue + return info.match_type + return None + + def set(self, **kwargs): + """ + Set match field parameter. + + :param kwargs: parameters + :return: void + """ + for name, value in kwargs.items(): + self[name] = value + + def clear(self): + """ + Clear all match fields. + + :return: void + """ + self._mk.clear() + + def _count(self): + return len(self._mk) + + +class Action: + """ + P4 action. + """ + + def __init__(self, action_name=None): + self._init = False + if action_name is None: + raise UserError("Please provide name for action") + self.action_name = action_name + action_info = CONTEXT.get_action(action_name) + if action_info is None: + raise UserError(f"Unknown action '{action_name}'") + self._action_id = action_info.preamble.id + self._params = OrderedDict() + for param in action_info.params: + self._params[param.name] = param + self._action_info = action_info + self._param_values = OrderedDict() + self._set_docstring() + self._init = True + + def _set_docstring(self): + self.__doc__ = f"Action parameters for action '{self.action_name}':\n\n" + for _, info in self._params.items(): + self.__doc__ += str(info) + self.__doc__ += "\n\n" + self.__doc__ += "Set a param value with " \ + "<self>['<param_name>'] = '<value>'\n" + self.__doc__ += "You may also use <self>.set(<param_name>='<value>')\n" + + def _get_param(self, name): + if name not in self._params: + raise UserError("'{name}' is not a valid action parameter name " + "for action '{self._action_name}'") + return self._params[name] + + def __setattr__(self, name, value): + if name[0] == "_" or not self._init: + super().__setattr__(name, value) + return + if name == "action_name": + raise UserError("Cannot change action name") + super().__setattr__(name, value) + + def __setitem__(self, name, value): + param_info = self._get_param(name) + self._param_values[name] = self._parse_param(value, param_info) + print(self._param_values[name]) + + def __getitem__(self, name): + _ = self._get_param(name) + print(self._param_values.get(name, "Unset")) + + def _parse_param(self, s, param_info): + if not isinstance(s, str): + raise UserError("Action parameter value must be a string") + v = encode(s, param_info.bitwidth) + p = p4runtime_pb2.Action.Param() + p.param_id = param_info.id + p.value = make_canonical_if_option_set(v) + return p + + def msg(self): + """ + Create an action message. + + :return: action message + """ + msg = p4runtime_pb2.Action() + msg.action_id = self._action_id + msg.params.extend(self._param_values.values()) + return msg + + def _from_msg(self, msg): + assert self._action_id == msg.action_id + self._params.clear() + for p in msg.params: + p_name = CONTEXT.get_param_name(self.action_name, p.param_id) + self._param_values[p_name] = p + + def __str__(self): + return str(self.msg()) + + def id(self): + """ + Get action ID. + + :return: action ID + """ + return self._action_info.preamble.id + + def alias(self): + """ + Get action alias. + + :return: action alias + """ + return str(self._action_info.preamble.alias) + + def set(self, **kwargs): + """ + Set action parameters. + + :param kwargs: parameters + :return: void + """ + for name, value in kwargs.items(): + self[name] = value + + +class _EntityBase: + """ + Basic entity. + """ + + def __init__(self, entity_type, p4runtime_cls, modify_only=False): + self._init = False + self._entity_type = entity_type + self._entry = p4runtime_cls() + self._modify_only = modify_only + + def __dir__(self): + d = ["msg", "read"] + if self._modify_only: + d.append("modify") + else: + d.extend(["insert", "modify", "delete"]) + return d + + # to be called before issuing a P4Runtime request + # enforces checks that cannot be performed when setting individual fields + def _validate_msg(self): + return True + + def _update_msg(self): + pass + + def __getattr__(self, name): + raise AttributeError(f"'{self.__class__.__name__}' object " + f"has no attribute '{name}'") + + def msg(self): + """ + Get a basic entity message. + + :return: entity message + """ + self._update_msg() + return self._entry + + def _write(self, type_): + self._update_msg() + self._validate_msg() + update = p4runtime_pb2.Update() + update.type = type_ + getattr(update.entity, self._entity_type.name).CopyFrom(self._entry) + CLIENT.write_update(update) + + def insert(self): + """ + Insert an entity. + + :return: void + """ + if self._modify_only: + raise NotImplementedError( + f"Insert not supported for {self._entity_type.name}") + logging.debug("Inserting entry") + self._write(p4runtime_pb2.Update.INSERT) + + def delete(self): + """ + Delete an entity. + + :return: void + """ + if self._modify_only: + raise NotImplementedError( + f"Delete not supported for {self._entity_type.name}") + logging.debug("Deleting entry") + self._write(p4runtime_pb2.Update.DELETE) + + def modify(self): + """ + Modify an entity. + + :return: void + """ + logging.debug("Modifying entry") + self._write(p4runtime_pb2.Update.MODIFY) + + def _from_msg(self, msg): + raise NotImplementedError + + def read(self, function=None): + """ + Read an entity. + + :param function: function to read (optional) + :return: retrieved entity + """ + # Entities should override this method and provide a helpful docstring + self._update_msg() + self._validate_msg() + entity = p4runtime_pb2.Entity() + getattr(entity, self._entity_type.name).CopyFrom(self._entry) + + iterator = CLIENT.read_one(entity) + + # Cannot use a (simpler) generator here as we need to + # decorate __next__ with @parse_p4runtime_error. + class _EntryIterator: + def __init__(self, entity, it): + self._entity = entity + self._it = it + self._entities_it = None + + def __iter__(self): + return self + + @parse_p4runtime_error + def __next__(self): + if self._entities_it is None: + rep = next(self._it) + self._entities_it = iter(rep.entities) + try: + entity = next(self._entities_it) + except StopIteration: + self._entities_it = None + return next(self) + + if isinstance(self._entity, _P4EntityBase): + ent = type(self._entity)( + self._entity.name) # create new instance of same entity + else: + ent = type(self._entity)() + msg = getattr(entity, self._entity._entity_type.name) + ent._from_msg(msg) + # neither of these should be needed + # ent._update_msg() + # ent._entry.CopyFrom(msg) + return ent + + if function is None: + return _EntryIterator(self, iterator) + for x in _EntryIterator(self, iterator): + function(x) + + +class _P4EntityBase(_EntityBase): + """ + Basic P4 entity. + """ + + def __init__(self, p4_type, entity_type, p4runtime_cls, name=None, + modify_only=False): + super().__init__(entity_type, p4runtime_cls, modify_only) + self._p4_type = p4_type + if name is None: + raise UserError( + f"Please provide name for {p4_type.pretty_name}") + self.name = name + self._info = P4Objects(p4_type)[name] + self.id = self._info.id + + def __dir__(self): + return super().__dir__() + ["name", "id", "info"] + + def _from_msg(self, msg): + raise NotImplementedError + + def info(self): + """ + Display P4Info entry for the object. + + :return: P4 info entry + """ + return self._info + + +class ActionProfileMember(_P4EntityBase): + """ + P4 action profile member. + """ + + def __init__(self, action_profile_name=None): + super().__init__( + P4Type.action_profile, P4RuntimeEntity.action_profile_member, + p4runtime_pb2.ActionProfileMember, action_profile_name) + self.member_id = 0 + self.action = None + self._valid_action_ids = self._get_action_set() + self.__doc__ = f""" +An action profile member for '{action_profile_name}' + +Use <self>.info to display the P4Info entry for the action profile. + +Set the member id with <self>.member_id = <expr>. + +To set the action specification <self>.action = <instance of type Action>. +To set the value of action parameters, +use <self>.action['<param name>'] = <expr>. +Type <self>.action? for more details. + + +Typical usage to insert an action profile member: +m = action_profile_member['<action_profile_name>'](action='<action_name>', +member_id=1) +m.action['<p1>'] = ... +... +m.action['<pM>'] = ... +# OR m.action.set(p1=..., ..., pM=...) +m.insert + +For information about how to read members, use <self>.read? +""" + self._init = True + + def __dir__(self): + return super().__dir__() + ["member_id", "action"] + + def _get_action_set(self): + t_id = self._info.table_ids[0] + t_name = CONTEXT.get_name_from_id(t_id) + t = CONTEXT.get_table(t_name) + return {action.id for action in t.action_refs} + + def __call__(self, **kwargs): + for name, value in kwargs.items(): + if name == "action" and isinstance(value, str): + value = Action(value) + setattr(self, name, value) + return self + + def __setattr__(self, name, value): + if name[0] == "_" or not self._init: + super().__setattr__(name, value) + return + if name == "name": + raise UserError("Cannot change action profile name") + if name == "member_id": + if not isinstance(value, int): + raise UserError("member_id must be an integer") + if name == "action" and value is not None: + if not isinstance(value, Action): + raise UserError("action must be an instance of Action") + if not self._is_valid_action_id(value._action_id): + raise UserError(f"action '{value.action_name}' is not a valid " + f"action for this action profile") + super().__setattr__(name, value) + + def _is_valid_action_id(self, action_id): + return action_id in self._valid_action_ids + + def _update_msg(self): + self._entry.action_profile_id = self.id + self._entry.member_id = self.member_id + if self.action is not None: + self._entry.action.CopyFrom(self.action.msg()) + + def _from_msg(self, msg): + self.member_id = msg.member_id + if msg.HasField('action'): + action = msg.action + action_name = CONTEXT.get_name_from_id(action.action_id) + self.action = Action(action_name) + self.action._from_msg(action) + + def read(self, function=None): + """Generate a P4Runtime Read RPC. Supports wildcard reads (just leave + the appropriate fields unset). + + If function is None, returns an iterator. Iterate over it to get all the + members (as ActionProfileMember instances) returned by the + server. Otherwise, function is applied to all the members returned + by the server. + """ + return super().read(function) + + +class GroupMember: + """ + P4 group member. + + A member in an ActionProfileGroup. + Construct with GroupMember(<member_id>, weight=<weight>, watch=<watch>, + watch_port=<watch_port>). + You can set / get attributes member_id (required), weight (default 1), + watch (default 0), watch_port (default ""). + """ + + def __init__(self, member_id=None, weight=1, watch=0, watch_port=b""): + if member_id is None: + raise UserError("member_id is required") + self._msg = p4runtime_pb2.ActionProfileGroup.Member() + self._msg.member_id = member_id + self._msg.weight = weight + if watch: + self._msg.watch = watch + if watch_port: + self._msg.watch_port = watch_port + + def __dir__(self): + return ["member_id", "weight", "watch", "watch_port"] + + def __setattr__(self, name, value): + if name[0] == "_": + super().__setattr__(name, value) + return + if name == "member_id": + if not isinstance(value, int): + raise UserError("member_id must be an integer") + self._msg.member_id = value + return + if name == "weight": + if not isinstance(value, int): + raise UserError("weight must be an integer") + self._msg.weight = value + return + if name == "watch": + if not isinstance(value, int): + raise UserError("watch must be an integer") + self._msg.watch = value + return + if name == "watch_port": + if not isinstance(value, bytes): + raise UserError("watch_port must be a byte string") + self._msg.watch_port = value + return + super().__setattr__(name, value) + + def __getattr__(self, name): + if name == "member_id": + return self._msg.member_id + if name == "weight": + return self._msg.weight + if name == "watch": + return self._msg.watch + if name == "watch_port": + return self._msg.watch_port + return super().__getattr__(name) + + def __str__(self): + return str(self._msg) + + +class ActionProfileGroup(_P4EntityBase): + """ + P4 action profile group. + """ + + def __init__(self, action_profile_name=None): + super().__init__( + P4Type.action_profile, P4RuntimeEntity.action_profile_group, + p4runtime_pb2.ActionProfileGroup, action_profile_name) + self.group_id = 0 + self.max_size = 0 + self.members = [] + self.__doc__ = f""" +An action profile group for '{action_profile_name}' + +Use <self>.info to display the P4Info entry for the action profile. + +Set the group id with <self>.group_id = <expr>. Default is 0. +Set the max size with <self>.max_size = <expr>. Default is 0. + +Add members to the group with <self>.add(<member_id>, weight=<weight>, watch=<watch>, +watch_port=<watch_port>). +weight, watch and watch port are optional (default to 1, 0 and "" respectively). + +Typical usage to insert an action profile group: +g = action_profile_group['<action_profile_name>'](group_id=1) +g.add(<member id 1>) +g.add(<member id 2>) +# OR g.add(<member id 1>).add(<member id 2>) + +For information about how to read groups, use <self>.read? +""" + self._init = True + + def __dir__(self): + return super().__dir__() + ["group_id", "max_size", "members", "add", + "clear"] + + def __call__(self, **kwargs): + for name, value in kwargs.items(): + setattr(self, name, value) + return self + + def __setattr__(self, name, value): + if name[0] == "_" or not self._init: + super().__setattr__(name, value) + return + if name == "name": + raise UserError("Cannot change action profile name") + if name == "group_id": + if not isinstance(value, int): + raise UserError("group_id must be an integer") + if name == "members": + if not isinstance(value, list): + raise UserError("members must be a list of GroupMember objects") + for member in value: + if not isinstance(member, GroupMember): + raise UserError( + "members must be a list of GroupMember objects") + super().__setattr__(name, value) + + def add(self, member_id=None, weight=1, watch=0, watch_port=b""): + """Add a member to the members list.""" + self.members.append(GroupMember(member_id, weight, watch, watch_port)) + return self + + def clear(self): + """Empty members list.""" + self.members = [] + + def _update_msg(self): + self._entry.action_profile_id = self.id + self._entry.group_id = self.group_id + self._entry.max_size = self.max_size + del self._entry.members[:] + for member in self.members: + if not isinstance(member, GroupMember): + raise UserError("members must be a list of GroupMember objects") + m = self._entry.members.add() + m.CopyFrom(member._msg) + + def _from_msg(self, msg): + self.group_id = msg.group_id + self.max_size = msg.max_size + self.members = [] + for member in msg.members: + self.add(member.member_id, member.weight, member.watch, + member.watch_port) + + def read(self, function=None): + """Generate a P4Runtime Read RPC. Supports wildcard reads (just leave + the appropriate fields unset). + + If function is None, returns an iterator. Iterate over it to get all the + members (as ActionProfileGroup instances) returned by the + server. Otherwise, function is applied to all the groups returned by the + server. + """ + return super().read(function) + + +def _get_action_profile(table_name): + table = CONTEXT.get_table(table_name) + implementation_id = table.implementation_id + if implementation_id == 0: + return None + try: + implementation_name = CONTEXT.get_name_from_id(implementation_id) + except KeyError as ex: + raise InvalidP4InfoError( + f"Invalid implementation_id {implementation_id} for " + f"table '{table_name}'") from ex + ap = CONTEXT.get_obj(P4Type.action_profile, implementation_name) + if ap is None: + raise InvalidP4InfoError( + f"Unknown implementation for table '{table_name}'") + return ap + + +class OneshotAction: + """ + A P4 action in a oneshot action set. + Construct with OneshotAction(<action (Action instance)>, + weight=<weight>, watch=<watch>, watch_port=<watch_port>). + You can set / get attributes action (required), weight (default 1), + watch (default 0), watch_port (default ""). + """ + + def __init__(self, action=None, weight=1, watch=0, watch_port=b""): + if action is None: + raise UserError("action is required") + self.action = action + self.weight = weight + self.watch = watch + self.watch_port = watch_port + + def __dir__(self): + return ["action", "weight", "watch", "watch_port", "msg"] + + def __setattr__(self, name, value): + if name[0] == "_": + super().__setattr__(name, value) + return + if name == "action": + if not isinstance(value, Action): + raise UserError("action must be an instance of Action") + elif name == "weight": + if not isinstance(value, int): + raise UserError("weight must be an integer") + elif name == "watch": + if not isinstance(value, int): + raise UserError("watch must be an integer") + elif name == "watch_port": + print(type(value), value) + if not isinstance(value, bytes): + raise UserError("watch_port must be a byte string") + super().__setattr__(name, value) + + def msg(self): + """ + Create an one shot action message. + + :return: one shot action message + """ + msg = p4runtime_pb2.ActionProfileAction() + msg.action.CopyFrom(self.action.msg()) + msg.weight = self.weight + if self.watch: + msg.watch = self.watch + if self.watch_port: + msg.watch_port = self.watch_port + return msg + + def __str__(self): + return str(self.msg()) + + +class Oneshot: + """ + One shot action set. + """ + + def __init__(self, table_name=None): + self._init = False + if table_name is None: + raise UserError("Please provide table name") + self.table_name = table_name + self.actions = [] + self._table_info = P4Objects(P4Type.table)[table_name] + ap = _get_action_profile(table_name) + if not ap: + raise UserError("Cannot create Oneshot instance for a direct table") + if not ap.with_selector: + raise UserError( + "Cannot create Oneshot instance for a table " + "with an action profile without selector") + self.__doc__ = f""" +A "oneshot" action set for table '{self.table_name}'. + +To add an action to the set, use <self>.add(<Action instance>). +You can also access the set of actions with <self>.actions (which is a Python list). +""" + self._init = True + + def __dir__(self): + return ["table_name", "actions", "add", "msg"] + + def __setattr__(self, name, value): + if name[0] == "_" or not self._init: + super().__setattr__(name, value) + return + if name == "table_name": + raise UserError("Cannot change table name") + if name == "actions": + if not isinstance(value, list): + raise UserError( + "actions must be a list of OneshotAction objects") + for member in value: + if not isinstance(member, OneshotAction): + raise UserError( + "actions must be a list of OneshotAction objects") + if not self._is_valid_action_id(value.action._action_id): + raise UserError( + f"action '{value.action.action_name}' is not a valid " + f"action for table {self.table_name}") + super().__setattr__(name, value) + + def _is_valid_action_id(self, action_id): + for action_ref in self._table_info.action_refs: + if action_id == action_ref.id: + return True + return False + + def add(self, action=None, weight=1, watch=0, watch_port=b""): + """ + Add an action to the oneshot action set. + + :param action: action object + :param weight: weight (integer) + :param watch: watch (integer) + :param watch_port: watch port + :return: + """ + self.actions.append(OneshotAction(action, weight, watch, watch_port)) + return self + + def msg(self): + """ + Create an action profile message. + + :return: action profile message + """ + msg = p4runtime_pb2.ActionProfileActionSet() + msg.action_profile_actions.extend( + [action.msg() for action in self.actions]) + return msg + + def _from_msg(self, msg): + for action in msg.action_profile_actions: + action_name = CONTEXT.get_name_from_id(action.action.action_id) + a = Action(action_name) + a._from_msg(action.action) + self.actions.append(OneshotAction(a, action.weight, action.watch, + action.watch_port)) + + def __str__(self): + return str(self.msg()) + + +class _CounterData: + """ + P4 counter data. + """ + + @staticmethod + def attrs_for_counter_type(counter_type): + """ + Return counter attributes. + + :param counter_type: P4 counter type + :return: list of counter attributes + """ + attrs = [] + if counter_type in {p4info_pb2.CounterSpec.BYTES, + p4info_pb2.CounterSpec.BOTH}: + attrs.append("byte_count") + if counter_type in {p4info_pb2.CounterSpec.PACKETS, + p4info_pb2.CounterSpec.BOTH}: + attrs.append("packet_count") + return attrs + + def __init__(self, counter_name, counter_type): + self._counter_name = counter_name + self._counter_type = counter_type + self._msg = p4runtime_pb2.CounterData() + self._attrs = _CounterData.attrs_for_counter_type(counter_type) + + def __dir__(self): + return self._attrs + + def __setattr__(self, name, value): + if name[0] == "_": + super().__setattr__(name, value) + return + if name not in self._attrs: + type_name = p4info_pb2._COUNTERSPEC_UNIT.values_by_number[ + self._counter_type].name + raise UserError( + f"Counter '{self._counter_name}' is of type '{type_name}', " + f"you cannot set '{name}'") + if not isinstance(value, int): + raise UserError(f"{name} must be an integer") + setattr(self._msg, name, value) + + def __getattr__(self, name): + if name in ("byte_count", "packet_count"): + return getattr(self._msg, name) + raise AttributeError(f"'{self.__class__.__name__}' object has no " + f"attribute '{name}'") + + def msg(self): + """ + Create a counter data message. + + :return: counter data message + """ + return self._msg + + def _from_msg(self, msg): + self._msg.CopyFrom(msg) + + def __str__(self): + return str(self.msg()) + + @classmethod + def set_count(cls, instance, counter_name, counter_type, name, value): + """ + Set the value of a certain counter. + + :param instance: counter instance + :param counter_name: counter name + :param counter_type: counter type + :param name: counter attribute name + :param value: counter attribute value + :return: updated counter instance + """ + if instance is None: + d = cls(counter_name, counter_type) + else: + d = instance + setattr(d, name, value) + return d + + @classmethod + def get_count(cls, instance, counter_name, counter_type, name): + """ + Get the value of a certain counter. + + :param instance: + :param counter_name: counter name + :param counter_type: counter type + :param name: counter attribute name + :return: counter name and value + """ + if instance is None: + d = cls(counter_name, counter_type) + else: + d = instance + r = getattr(d, name) + return d, r + + +class _MeterConfig: + """ + P4 meter configuration. + """ + + @staticmethod + def attrs(): + """ + Get the attributes in this scope. + + :return: list of scope attributes + """ + return ["cir", "cburst", "pir", "pburst"] + + def __init__(self, meter_name, meter_type): + self._meter_name = meter_name + self._meter_type = meter_type + self._msg = p4runtime_pb2.MeterConfig() + self._attrs = _MeterConfig.attrs() + + def __dir__(self): + return self._attrs + + def __setattr__(self, name, value): + if name[0] == "_": + super().__setattr__(name, value) + return + if name in self._attrs: + if not isinstance(value, int): + raise UserError(f"{name} must be an integer") + setattr(self._msg, name, value) + + def __getattr__(self, name): + if name in self._attrs: + return getattr(self._msg, name) + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def msg(self): + """ + Create a meter config message. + + :return: meter config message + """ + return self._msg + + def _from_msg(self, msg): + self._msg.CopyFrom(msg) + + def __str__(self): + return str(self.msg()) + + @classmethod + def set_param(cls, instance, meter_name, meter_type, name, value): + """ + Set the value of a certain meter parameter. + + :param instance: meter instance + :param meter_name: meter name + :param meter_type: meter type + :param name: meter parameter name + :param value: meter parameter value + :return: updated meter + """ + if instance is None: + d = cls(meter_name, meter_type) + else: + d = instance + setattr(d, name, value) + return d + + @classmethod + def get_param(cls, instance, meter_name, meter_type, name): + """ + Get the value of a certain meter parameter. + + :param instance: meter instance + :param meter_name: meter name + :param meter_type: meter type + :param name: meter parameter name + :return: meter with parameter + """ + if instance is None: + d = cls(meter_name, meter_type) + else: + d = instance + r = getattr(d, name) + return d, r + + +class _IdleTimeout: + """ + P4 idle timeout. + """ + + @staticmethod + def attrs(): + """ + Get the attributes in this scope. + + :return: list of scope attributes + """ + return ["elapsed_ns"] + + def __init__(self): + self._msg = p4runtime_pb2.TableEntry.IdleTimeout() + self._attrs = _IdleTimeout.attrs() + + def __dir__(self): + return self._attrs + + def __setattr__(self, name, value): + if name[0] == "_": + super().__setattr__(name, value) + return + if name in self._attrs: + if not isinstance(value, int): + raise UserError(f"{name} must be an integer") + setattr(self._msg, name, value) + + def __getattr__(self, name): + if name in self._attrs: + return getattr(self._msg, name) + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'") + + def msg(self): + """ + Create an idle timeout message. + + :return: idle timeout message + """ + return self._msg + + def _from_msg(self, msg): + self._msg.CopyFrom(msg) + + def __str__(self): + return str(self.msg()) + + @classmethod + def set_param(cls, instance, name, value): + """ + Set the value of a certain idle timeout parameter. + + :param instance: idle timeout instance + :param name: idle timeout parameter name + :param value: idle timeout parameter value + :return: updated idle timeout instance + """ + if instance is None: + d = cls() + else: + d = instance + setattr(d, name, value) + return d + + @classmethod + def get_param(cls, instance, name): + """ + Set the value of a certain idle timeout parameter. + + :param instance: idle timeout instance + :param name: idle timeout parameter name + :return: idle timeout instance with parameter + """ + if instance is None: + d = cls() + else: + d = instance + r = getattr(d, name) + return d, r + + +class TableEntry(_P4EntityBase): + """ + P4 table entry. + """ + + @enum.unique + class _ActionSpecType(enum.Enum): + NONE = 0 + DIRECT_ACTION = 1 + MEMBER_ID = 2 + GROUP_ID = 3 + ONESHOT = 4 + + @classmethod + def _action_spec_name_to_type(cls, name): + return { + "action": cls._ActionSpecType.DIRECT_ACTION, + "member_id": cls._ActionSpecType.MEMBER_ID, + "group_id": cls._ActionSpecType.GROUP_ID, + "oneshot": cls._ActionSpecType.ONESHOT, + }.get(name, None) + + def __init__(self, table_name=None): + super().__init__( + P4Type.table, P4RuntimeEntity.table_entry, + p4runtime_pb2.TableEntry, table_name) + self.match = MatchKey(table_name, self._info.match_fields) + self._action_spec_type = self._ActionSpecType.NONE + self._action_spec = None + self.action: Action + self.member_id = -1 + self.group_id = -1 + self.oneshot = None + self.priority = 0 + self.is_default = False + ap = _get_action_profile(table_name) + if ap is None: + self._support_members = False + self._support_groups = False + else: + self._support_members = True + self._support_groups = ap.with_selector + self._direct_counter = None + self._direct_meter = None + for res_id in self._info.direct_resource_ids: + prefix = (res_id & 0xff000000) >> 24 + if prefix == p4info_pb2.P4Ids.DIRECT_COUNTER: + self._direct_counter = CONTEXT.get_obj_by_id(res_id) + elif prefix == p4info_pb2.P4Ids.DIRECT_METER: + self._direct_meter = CONTEXT.get_obj_by_id(res_id) + self._counter_data = None + self._meter_config = None + self.idle_timeout_ns = 0 + self._time_since_last_hit = None + self._idle_timeout_behavior = None + table = CONTEXT.get_table(table_name) + if table.idle_timeout_behavior > 0: + self._idle_timeout_behavior = table.idle_timeout_behavior + self.metadata = b"" + self.__doc__ = f""" +An entry for table '{table_name}' + +Use <self>.info to display the P4Info entry for this table. + +To set the match key, use <self>.match['<field name>'] = <expr>. +Type <self>.match? for more details. +""" + if self._direct_counter is not None: + self.__doc__ += """ +To set the counter spec, use <self>.counter_data.byte_count and/or <self>.counter_data.packet_count. +To unset it, use <self>.counter_data = None or <self>.clear_counter_data(). +""" + if self._direct_meter is not None: + self.__doc__ += """ +To access the meter config, use <self>.meter_config.<cir|cburst|pir|pburst>. +To unset it, use <self>.meter_config = None or <self>.clear_meter_config(). +""" + if ap is None: + self.__doc__ += """ +To set the action specification (this is a direct table): +<self>.action = <instance of type Action>. +To set the value of action parameters, use <self>.action['<param name>'] = <expr>. +Type <self>.action? for more details. +""" + if self._support_members: + self.__doc__ += """ +Access the member_id with <self>.member_id. +""" + if self._support_groups: + self.__doc__ += """ +Or access the group_id with <self>.group_id. +""" + if self._idle_timeout_behavior is not None: + self.__doc__ += """ +To access the time this entry was last hit, use <self>.time_since_last_hit.elapsed_ns. +To unset it, use <self>.time_since_last_hit = None or <self>.clear_time_since_last_hit(). +""" + self.__doc__ += """ +To set the priority, use <self>.priority = <expr>. + +To mark the entry as default, use <self>.is_default = True. + +To add an idle timeout to the entry, use <self>.idle_timeout_ns = <expr>. + +To add metadata to the entry, use <self>.metadata = <expr>. +""" + if ap is None: + self.__doc__ += """ +Typical usage to insert a table entry: +t = table_entry['<table_name>'](action='<action_name>') +t.match['<f1>'] = ... +... +t.match['<fN>'] = ... +# OR t.match.set(f1=..., ..., fN=...) +t.action['<p1>'] = ... +... +t.action['<pM>'] = ... +# OR t.action.set(p1=..., ..., pM=...) +t.insert + +Typical usage to set the default entry: +t = table_entry['<table_name>'](is_default=True) +t.action['<p1>'] = ... +... +t.action['<pM>'] = ... +# OR t.action.set(p1=..., ..., pM=...) +t.modify +""" + else: + self.__doc__ += """ +Typical usage to insert a table entry: +t = table_entry['<table_name>'] +t.match['<f1>'] = ... +... +t.match['<fN>'] = ... +# OR t.match.set(f1=..., ..., fN=...) +t.member_id = <expr> +""" + self.__doc__ += """ +For information about how to read table entries, use <self>.read? +""" + + self._init = True + + def __dir__(self): + d = super().__dir__() + [ + "match", "priority", "is_default", "idle_timeout_ns", "metadata", + "clear_action", "clear_match", "clear_counter_data", + "clear_meter_config", + "clear_time_since_last_hit"] + if self._support_groups: + d.extend(["member_id", "group_id", "oneshot"]) + elif self._support_members: + d.append("member_id") + else: + d.append("action") + if self._direct_counter is not None: + d.append("counter_data") + if self._direct_meter is not None: + d.append("meter_config") + if self._idle_timeout_behavior is not None: + d.append("time_since_last_hit") + return d + + def __call__(self, **kwargs): + for name, value in kwargs.items(): + if name == "action" and isinstance(value, str): + value = Action(value) + setattr(self, name, value) + return self + + def _action_spec_set_member(self, member_id): + if isinstance(member_id, type(None)): + if self._action_spec_type == self._ActionSpecType.MEMBER_ID: + super().__setattr__("_action_spec_type", + self._ActionSpecType.NONE) + super().__setattr__("_action_spec", None) + return + if not isinstance(member_id, int): + raise UserError("member_id must be an integer") + if not self._support_members: + raise UserError("Table does not have an action profile and " + "therefore does not support members") + super().__setattr__("_action_spec_type", self._ActionSpecType.MEMBER_ID) + super().__setattr__("_action_spec", member_id) + + def _action_spec_set_group(self, group_id): + if isinstance(group_id, type(None)): + if self._action_spec_type == self._ActionSpecType.GROUP_ID: + super().__setattr__("_action_spec_type", + self._ActionSpecType.NONE) + super().__setattr__("_action_spec", None) + return + if not isinstance(group_id, int): + raise UserError("group_id must be an integer") + if not self._support_groups: + raise UserError( + "Table does not have an action profile with selector " + "and therefore does not support groups") + super().__setattr__("_action_spec_type", self._ActionSpecType.GROUP_ID) + super().__setattr__("_action_spec", group_id) + + def _action_spec_set_action(self, action): + if isinstance(action, type(None)): + if self._action_spec_type == self._ActionSpecType.DIRECT_ACTION: + super().__setattr__("_action_spec_type", + self._ActionSpecType.NONE) + super().__setattr__("_action_spec", None) + return + if not isinstance(action, Action): + raise UserError("action must be an instance of Action") + if self._info.implementation_id != 0: + raise UserError( + "Table has an implementation and therefore " + "does not support direct actions (P4Runtime 1.0 doesn't " + "support writing the default action for indirect tables") + if not self._is_valid_action_id(action._action_id): + raise UserError(f"action '{action.action_name}' is not a valid " + f"action for this table") + super().__setattr__("_action_spec_type", + self._ActionSpecType.DIRECT_ACTION) + super().__setattr__("_action_spec", action) + + def _action_spec_set_oneshot(self, oneshot): + if isinstance(oneshot, type(None)): + if self._action_spec_type == self._ActionSpecType.ONESHOT: + super().__setattr__("_action_spec_type", + self._ActionSpecType.NONE) + super().__setattr__("_action_spec", None) + return + if not isinstance(oneshot, Oneshot): + raise UserError("oneshot must be an instance of Oneshot") + if not self._support_groups: + raise UserError( + "Table does not have an action profile with selector " + "and therefore does not support oneshot programming") + if self.name != oneshot.table_name: + raise UserError( + "This Oneshot instance was not created for this table") + super().__setattr__("_action_spec_type", self._ActionSpecType.ONESHOT) + super().__setattr__("_action_spec", oneshot) + + def __setattr__(self, name, value): + if name[0] == "_" or not self._init: + super().__setattr__(name, value) + return + if name == "name": + raise UserError("Cannot change table name") + if name == "priority": + if not isinstance(value, int): + raise UserError("priority must be an integer") + if name == "match" and not isinstance(value, MatchKey): + raise UserError("match must be an instance of MatchKey") + if name == "is_default": + if not isinstance(value, bool): + raise UserError("is_default must be a boolean") + # TODO: handle other cases # pylint: disable=W0511 + # is_default is set to True)? + if value is True and self.match._count() > 0: + print("Clearing match key because entry is now default") + self.match.clear() + if name == "member_id": + self._action_spec_set_member(value) + return + if name == "group_id": + self._action_spec_set_group(value) + return + if name == "oneshot": + self._action_spec_set_oneshot(value) + if name == "action" and value is not None: + self._action_spec_set_action(value) + return + if name == "counter_data": + if self._direct_counter is None: + raise UserError("Table has no direct counter") + if value is None: + self._counter_data = None + return + raise UserError("Cannot set 'counter_data' directly") + if name == "meter_config": + if self._direct_meter is None: + raise UserError("Table has no direct meter") + if value is None: + self._meter_config = None + return + raise UserError("Cannot set 'meter_config' directly") + if name == "idle_timeout_ns": + if not isinstance(value, int): + raise UserError("idle_timeout_ns must be an integer") + if name == "time_since_last_hit": + if self._idle_timeout_behavior is None: + raise UserError("Table has no idle timeouts") + if value is None: + self._time_since_last_hit = None + return + raise UserError("Cannot set 'time_since_last_hit' directly") + if name == "metadata": + if not isinstance(value, bytes): + raise UserError("metadata must be a byte string") + super().__setattr__(name, value) + + def __getattr__(self, name): + if name == "counter_data": + if self._direct_counter is None: + raise UserError("Table has no direct counter") + if self._counter_data is None: + self._counter_data = _CounterData( + self._direct_counter.preamble.name, + self._direct_counter.spec.unit) + return self._counter_data + if name == "meter_config": + if self._direct_meter is None: + raise UserError("Table has no direct meter") + if self._meter_config is None: + self._meter_config = _MeterConfig( + self._direct_meter.preamble.name, + self._direct_meter.spec.unit) + return self._meter_config + if name == "time_since_last_hit": + if self._idle_timeout_behavior is None: + raise UserError("Table has no idle timeouts") + if self._time_since_last_hit is None: + self._time_since_last_hit = _IdleTimeout() + return self._time_since_last_hit + + t = self._action_spec_name_to_type(name) + if t is None: + return super().__getattr__(name) + if self._action_spec_type == t: + return self._action_spec + if t == self._ActionSpecType.ONESHOT: + self._action_spec_type = self._ActionSpecType.ONESHOT + self._action_spec = Oneshot(self.name) + return self._action_spec + return None + + def _is_valid_action_id(self, action_id): + for action_ref in self._info.action_refs: + if action_id == action_ref.id: + return True + return False + + def _from_msg(self, msg): + self.priority = msg.priority + self.is_default = msg.is_default_action + self.idle_timeout_ns = msg.idle_timeout_ns + self.metadata = msg.metadata + for mf in msg.match: + mf_name = CONTEXT.get_mf_name(self.name, mf.field_id) + self.match._mk[mf_name] = mf + if msg.action.HasField('action'): + action = msg.action.action + action_name = CONTEXT.get_name_from_id(action.action_id) + self.action = Action(action_name) + self.action._from_msg(action) + elif msg.action.HasField('action_profile_member_id'): + self.member_id = msg.action.action_profile_member_id + elif msg.action.HasField('action_profile_group_id'): + self.group_id = msg.action.action_profile_group_id + elif msg.action.HasField('action_profile_action_set'): + self.oneshot = Oneshot(self.name) + self.oneshot._from_msg(msg.action.action_profile_action_set) + if msg.HasField('counter_data'): + self._counter_data = _CounterData( + self._direct_counter.preamble.name, + self._direct_counter.spec.unit) + self._counter_data._from_msg(msg.counter_data) + else: + self._counter_data = None + if msg.HasField('meter_config'): + self._meter_config = _MeterConfig( + self._direct_meter.preamble.name, self._direct_meter.spec.unit) + self._meter_config._from_msg(msg.meter_config) + else: + self._meter_config = None + if msg.HasField("time_since_last_hit"): + self._time_since_last_hit = _IdleTimeout() + self._time_since_last_hit._from_msg(msg.time_since_last_hit) + else: + self._time_since_last_hit = None + + def read(self, function=None): + """Generate a P4Runtime Read RPC. Supports wildcard reads (just leave + the appropriate fields unset). + If function is None, returns an iterator. Iterate over it to get all the + table entries (TableEntry instances) returned by the server. Otherwise, + function is applied to all the table entries returned by the server. + + For example: + for te in <self>.read(): + print(te) + The above code is equivalent to the following one-liner: + <self>.read(lambda te: print(te)) + + To delete all the entries from a table, simply use: + table_entry['<table_name>'].read(function=lambda x: x.delete()) + """ + return super().read(function) + + def _update_msg(self): + entry = p4runtime_pb2.TableEntry() + entry.table_id = self.id + entry.match.extend(self.match._mk.values()) + entry.priority = self.priority + entry.is_default_action = self.is_default + entry.idle_timeout_ns = self.idle_timeout_ns + entry.metadata = self.metadata + if self._action_spec_type == self._ActionSpecType.DIRECT_ACTION: + entry.action.action.CopyFrom(self._action_spec.msg()) + elif self._action_spec_type == self._ActionSpecType.MEMBER_ID: + entry.action.action_profile_member_id = self._action_spec + elif self._action_spec_type == self._ActionSpecType.GROUP_ID: + entry.action.action_profile_group_id = self._action_spec + elif self._action_spec_type == self._ActionSpecType.ONESHOT: + entry.action.action_profile_action_set.CopyFrom( + self._action_spec.msg()) + if self._counter_data is None: + entry.ClearField('counter_data') + else: + entry.counter_data.CopyFrom(self._counter_data.msg()) + if self._meter_config is None: + entry.ClearField('meter_config') + else: + entry.meter_config.CopyFrom(self._meter_config.msg()) + if self._time_since_last_hit is None: + entry.ClearField("time_since_last_hit") + else: + entry.time_since_last_hit.CopyFrom(self._time_since_last_hit.msg()) + self._entry = entry + + def _validate_msg(self): + if self.is_default and self.match._count() > 0: + raise UserError("Match key must be empty for default entry, " + "use <self>.is_default = False " + "or <self>.match.clear " + "(whichever one is appropriate)") + + def clear_action(self): + """Clears the action spec for the TableEntry.""" + super().__setattr__("_action_spec_type", self._ActionSpecType.NONE) + super().__setattr__("_action_spec", None) + + def clear_match(self): + """Clears the match spec for the TableEntry.""" + self.match.clear() + + def clear_counter_data(self): + """Clear all counter data, same as <self>.counter_data = None""" + self._counter_data = None + + def clear_meter_config(self): + """Clear the meter config, same as <self>.meter_config = None""" + self._meter_config = None + + def clear_time_since_last_hit(self): + """Clear the idle timeout, same as <self>.time_since_last_hit = None""" + self._time_since_last_hit = None + + +class _CounterEntryBase(_P4EntityBase): + """ + Basic P4 counter entry. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._counter_type = self._info.spec.unit + self.packet_count = -1 + self.byte_count = -1 + self._data = None + + def __dir__(self): + return super().__dir__() + _CounterData.attrs_for_counter_type( + self._counter_type) + [ + "clear_data"] + + def __call__(self, **kwargs): + for name, value in kwargs.items(): + setattr(self, name, value) + return self + + def __setattr__(self, name, value): + if name[0] == "_" or not self._init: + super().__setattr__(name, value) + return + if name == "name": + raise UserError("Cannot change counter name") + if name in ("byte_count", "packet_count"): + self._data = _CounterData.set_count( + self._data, self.name, self._counter_type, name, value) + return + if name == "data": + if value is None: + self._data = None + return + raise UserError("Cannot set 'data' directly") + super().__setattr__(name, value) + + def __getattr__(self, name): + if name in ("byte_count", "packet_count"): + self._data, r = _CounterData.get_count( + self._data, self.name, self._counter_type, name) + return r + if name == "data": + if self._data is None: + self._data = _CounterData(self.name, self._counter_type) + return self._data + return super().__getattr__(name) + + def _from_msg(self, msg): + self._entry.CopyFrom(msg) + if msg.HasField('data'): + self._data = _CounterData(self.name, self._counter_type) + self._data._from_msg(msg.data) + else: + self._data = None + + def _update_msg(self): + if self._data is None: + self._entry.ClearField('data') + else: + self._entry.data.CopyFrom(self._data.msg()) + + def clear_data(self): + """Clear all counter data, same as <self>.data = None""" + self._data = None + + +class CounterEntry(_CounterEntryBase): + """ + P4 counter entry. + """ + + def __init__(self, counter_name=None): + super().__init__( + P4Type.counter, P4RuntimeEntity.counter_entry, + p4runtime_pb2.CounterEntry, counter_name, + modify_only=True) + self._entry.counter_id = self.id + self.index = -1 + self.__doc__ = f""" +An entry for counter '{counter_name}' + +Use <self>.info to display the P4Info entry for this counter. + +Set the index with <self>.index = <expr>. +To reset it (e.g. for wildcard read), set it to None. + +Access byte count and packet count with <self>.byte_count / <self>.packet_count. + +To read from the counter, use <self>.read +To write to the counter, use <self>.modify +""" + self._init = True + + def __dir__(self): + return super().__dir__() + ["index", "data"] + + def __setattr__(self, name, value): + if name == "index": + if value is None: + self._entry.ClearField('index') + return + if not isinstance(value, int): + raise UserError("index must be an integer") + self._entry.index.index = value + return + super().__setattr__(name, value) + + def __getattr__(self, name): + if name == "index": + return self._entry.index.index + return super().__getattr__(name) + + def read(self, function=None): + """Generate a P4Runtime Read RPC. Supports wildcard reads (just leave + the index unset). + If function is None, returns an iterator. Iterate over it to get all the + counter entries (CounterEntry instances) returned by the + server. Otherwise, function is applied to all the counter entries + returned by the server. + + For example: + for c in <self>.read(): + print(c) + The above code is equivalent to the following one-liner: + <self>.read(lambda c: print(c)) + """ + return super().read(function) + + +class DirectCounterEntry(_CounterEntryBase): + """ + Direct P4 counter entry. + """ + + def __init__(self, direct_counter_name=None): + super().__init__( + P4Type.direct_counter, P4RuntimeEntity.direct_counter_entry, + p4runtime_pb2.DirectCounterEntry, direct_counter_name, + modify_only=True) + self._direct_table_id = self._info.direct_table_id + try: + self._direct_table_name = CONTEXT.get_name_from_id( + self._direct_table_id) + except KeyError as ex: + raise InvalidP4InfoError(f"direct_table_id {self._direct_table_id} " + f"is not a valid table id") from ex + self._table_entry = TableEntry(self._direct_table_name) + self.__doc__ = f""" +An entry for direct counter '{direct_counter_name}' + +Use <self>.info to display the P4Info entry for this direct counter. + +Set the table_entry with <self>.table_entry = <TableEntry instance>. +The TableEntry instance must be for the table to which the direct counter is +attached. +To reset it (e.g. for wildcard read), set it to None. It is the same as: +<self>.table_entry = TableEntry({self._direct_table_name}) + +Access byte count and packet count with <self>.byte_count / <self>.packet_count. + +To read from the counter, use <self>.read +To write to the counter, use <self>.modify +""" + self._init = True + + def __dir__(self): + return super().__dir__() + ["table_entry"] + + def __setattr__(self, name, value): + if name == "index": + raise UserError("Direct counters are not index-based") + if name == "table_entry": + if value is None: + self._table_entry = TableEntry(self._direct_table_name) + return + if not isinstance(value, TableEntry): + raise UserError("table_entry must be an instance of TableEntry") + if value.name != self._direct_table_name: + raise UserError(f"This DirectCounterEntry is for " + f"table '{self._direct_table_name}'") + self._table_entry = value + return + super().__setattr__(name, value) + + def __getattr__(self, name): + if name == "index": + raise UserError("Direct counters are not index-based") + if name == "table_entry": + return self._table_entry + return super().__getattr__(name) + + def _update_msg(self): + super()._update_msg() + if self._table_entry is None: + self._entry.ClearField('table_entry') + else: + self._entry.table_entry.CopyFrom(self._table_entry.msg()) + + def _from_msg(self, msg): + super()._from_msg(msg) + if msg.HasField('table_entry'): + self._table_entry._from_msg(msg.table_entry) + else: + self._table_entry = None + + def read(self, function=None): + """Generate a P4Runtime Read RPC. Supports wildcard reads (just leave + the index unset). + If function is None, returns an iterator. Iterate over it to get all the + direct counter entries (DirectCounterEntry instances) returned by the + server. Otherwise, function is applied to all the direct counter entries + returned by the server. + + For example: + for c in <self>.read(): + print(c) + The above code is equivalent to the following one-liner: + <self>.read(lambda c: print(c)) + """ + return super().read(function) + + +class _MeterEntryBase(_P4EntityBase): + """ + Basic P4 meter entry. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._meter_type = self._info.spec.unit + self.index = -1 + self.cir = -1 + self.cburst = -1 + self.pir = -1 + self.pburst = -1 + self._config = None + + def __dir__(self): + return super().__dir__() + _MeterConfig.attrs() + ["clear_config"] + + def __call__(self, **kwargs): + for name, value in kwargs.items(): + setattr(self, name, value) + return self + + def __setattr__(self, name, value): + if name[0] == "_" or not self._init: + super().__setattr__(name, value) + return + if name == "name": + raise UserError("Cannot change meter name") + if name in _MeterConfig.attrs(): + self._config = _MeterConfig.set_param( + self._config, self.name, self._meter_type, name, value) + return + if name == "config": + if value is None: + self._config = None + return + raise UserError("Cannot set 'config' directly") + super().__setattr__(name, value) + + def __getattr__(self, name): + if name in _MeterConfig.attrs(): + self._config, r = _MeterConfig.get_param( + self._config, self.name, self._meter_type, name) + return r + if name == "config": + if self._config is None: + self._config = _MeterConfig(self.name, self._meter_type) + return self._config + return super().__getattr__(name) + + def _from_msg(self, msg): + self._entry.CopyFrom(msg) + if msg.HasField('config'): + self._config = _MeterConfig(self.name, self._meter_type) + self._config._from_msg(msg.config) + else: + self._config = None + + def _update_msg(self): + if self._config is None: + self._entry.ClearField('config') + else: + self._entry.config.CopyFrom(self._config.msg()) + + def clear_config(self): + """Clear the meter config, same as <self>.config = None""" + self._config = None + + +class MeterEntry(_MeterEntryBase): + """ + P4 meter entry. + """ + + def __init__(self, meter_name=None): + super().__init__( + P4Type.meter, P4RuntimeEntity.meter_entry, + p4runtime_pb2.MeterEntry, meter_name, + modify_only=True) + self._entry.meter_id = self.id + self.__doc__ = f""" +An entry for meter '{meter_name}' + +Use <self>.info to display the P4Info entry for this meter. + +Set the index with <self>.index = <expr>. +To reset it (e.g. for wildcard read), set it to None. + +Access meter rates and burst sizes with: +<self>.cir +<self>.cburst +<self>.pir +<self>.pburst + +To read from the meter, use <self>.read +To write to the meter, use <self>.modify +""" + self._init = True + + def __dir__(self): + return super().__dir__() + ["index", "config"] + + def __setattr__(self, name, value): + if name == "index": + if value is None: + self._entry.ClearField('index') + return + if not isinstance(value, int): + raise UserError("index must be an integer") + self._entry.index.index = value + return + super().__setattr__(name, value) + + def __getattr__(self, name): + if name == "index": + return self._entry.index.index + return super().__getattr__(name) + + def read(self, function=None): + """Generate a P4Runtime Read RPC. Supports wildcard reads (just leave + the index unset). + If function is None, returns an iterator. Iterate over it to get all the + meter entries (MeterEntry instances) returned by the + server. Otherwise, function is applied to all the meter entries + returned by the server. + + For example: + for c in <self>.read(): + print(c) + The above code is equivalent to the following one-liner: + <self>.read(lambda c: print(c)) + """ + return super().read(function) + + +class DirectMeterEntry(_MeterEntryBase): + """ + Direct P4 meter entry. + """ + + def __init__(self, direct_meter_name=None): + super().__init__( + P4Type.direct_meter, P4RuntimeEntity.direct_meter_entry, + p4runtime_pb2.DirectMeterEntry, direct_meter_name, + modify_only=True) + self._direct_table_id = self._info.direct_table_id + try: + self._direct_table_name = CONTEXT.get_name_from_id( + self._direct_table_id) + except KeyError as ex: + raise InvalidP4InfoError(f"direct_table_id {self._direct_table_id} " + f"is not a valid table id") from ex + self._table_entry = TableEntry(self._direct_table_name) + self.__doc__ = f""" +An entry for direct meter '{direct_meter_name}' + +Use <self>.info to display the P4Info entry for this direct meter. + +Set the table_entry with <self>.table_entry = <TableEntry instance>. +The TableEntry instance must be for the table to which the direct meter is attached. +To reset it (e.g. for wildcard read), set it to None. It is the same as: +<self>.table_entry = TableEntry({self._direct_table_name}) + +Access meter rates and burst sizes with: +<self>.cir +<self>.cburst +<self>.pir +<self>.pburst + +To read from the meter, use <self>.read +To write to the meter, use <self>.modify +""" + self._init = True + + def __dir__(self): + return super().__dir__() + ["table_entry"] + + def __setattr__(self, name, value): + if name == "index": + raise UserError("Direct meters are not index-based") + if name == "table_entry": + if value is None: + self._table_entry = TableEntry(self._direct_table_name) + return + if not isinstance(value, TableEntry): + raise UserError("table_entry must be an instance of TableEntry") + if value.name != self._direct_table_name: + raise UserError(f"This DirectMeterEntry is for " + f"table '{self._direct_table_name}'") + self._table_entry = value + return + super().__setattr__(name, value) + + def __getattr__(self, name): + if name == "index": + raise UserError("Direct meters are not index-based") + if name == "table_entry": + return self._table_entry + return super().__getattr__(name) + + def _update_msg(self): + super()._update_msg() + if self._table_entry is None: + self._entry.ClearField('table_entry') + else: + self._entry.table_entry.CopyFrom(self._table_entry.msg()) + + def _from_msg(self, msg): + super()._from_msg(msg) + if msg.HasField('table_entry'): + self._table_entry._from_msg(msg.table_entry) + else: + self._table_entry = None + + def read(self, function=None): + """Generate a P4Runtime Read RPC. Supports wildcard reads (just leave + the index unset). + If function is None, returns an iterator. Iterate over it to get all the + direct meter entries (DirectMeterEntry instances) returned by the + server. Otherwise, function is applied to all the direct meter entries + returned by the server. + + For example: + for c in <self>.read(): + print(c) + The above code is equivalent to the following one-liner: + <self>.read(lambda c: print(c)) + """ + return super().read(function) + + +class P4RuntimeEntityBuilder: + """ + P4 entity builder. + """ + + def __init__(self, obj_type, entity_type, entity_cls): + self._obj_type = obj_type + self._names = sorted([name for name, _ in CONTEXT.get_objs(obj_type)]) + self._entity_type = entity_type + self._entity_cls = entity_cls + self.__doc__ = f"""Construct a {entity_cls.__name__} entity +Usage: <var> = {entity_type.name}["<{obj_type.pretty_name} name>"] +This is equivalent to <var>={entity_cls.__name__}(<{obj_type.pretty_name} name>) +Use command '{obj_type.p4info_name}' to see list of {obj_type.pretty_names} +""" + + def _ipython_key_completions_(self): + return self._names + + def __getitem__(self, name): + obj = CONTEXT.get_obj(self._obj_type, name) + if obj is None: + raise UserError( + f"{self._obj_type.pretty_name} '{name}' does not exist") + return self._entity_cls(name) + + def __setitem__(self, name, value): + raise UserError("Operation not allowed") + + def __str__(self): + return f"Construct a {self.entity_cls.__name__} entity" + + +class Replica: + """ + A port "replica" (port number + instance id) used for multicast + and clone session programming. + Construct with Replica(egress_port, instance=<instance>). + You can set / get attributes egress_port (required), instance (default 0). + """ + + def __init__(self, egress_port=None, instance=0): + if egress_port is None: + raise UserError("egress_port is required") + self._msg = p4runtime_pb2.Replica() + self._msg.egress_port = egress_port + self._msg.instance = instance + + def __dir__(self): + return ["port", "egress_port", "instance"] + + def __setattr__(self, name, value): + if name[0] == "_": + super().__setattr__(name, value) + return + if name in ("egress_port", "port"): + if not isinstance(value, int): + raise UserError("egress_port must be an integer") + self._msg.egress_port = value + return + if name == "instance": + if not isinstance(value, int): + raise UserError("instance must be an integer") + self._msg.instance = value + return + super().__setattr__(name, value) + + def __getattr__(self, name): + if name in ("egress_port", "port"): + return self._msg.egress_port + if name == "instance": + return self._msg.instance + return super().__getattr__(name) + + def __str__(self): + return str(self._msg) + + +class MulticastGroupEntry(_EntityBase): + """ + P4 multicast group entry. + """ + + def __init__(self, group_id=0): + super().__init__( + P4RuntimeEntity.packet_replication_engine_entry, + p4runtime_pb2.PacketReplicationEngineEntry) + self.group_id = group_id + self.replicas = [] + self.__doc__ = """ +Multicast group entry. +Create an instance with multicast_group_entry(<group_id>). +Add replicas with <self>.add(<eg_port_1>, <instance_1>).add(<eg_port_2>, <instance_2>)... +""" + self._init = True + + def __dir__(self): + return ["group_id", "replicas"] + + def __setattr__(self, name, value): + if name[0] == "_": + super().__setattr__(name, value) + return + if name == "group_id": + if not isinstance(value, int): + raise UserError("group_id must be an integer") + if name == "replicas": + if not isinstance(value, list): + raise UserError("replicas must be a list of Replica objects") + for r in value: + if not isinstance(r, Replica): + raise UserError( + "replicas must be a list of Replica objects") + super().__setattr__(name, value) + + def _from_msg(self, msg): + self.group_id = msg.multicast_group_entry.multicast_group_id + for r in msg.multicast_group_entry.replicas: + self.add(r.egress_port, r.instance) + + def read(self, function=None): + """Generate a P4Runtime Read RPC. Supports wildcard reads (just leave + the group_id as 0). + If function is None, returns an iterator. Iterate over it to get all the + multicast group entries (MulticastGroupEntry instances) returned by the + server. Otherwise, function is applied to all the multicast group entries + returned by the server. + + For example: + for c in <self>.read(): + print(c) + The above code is equivalent to the following one-liner: + <self>.read(lambda c: print(c)) + """ + return super().read(function) + + def _update_msg(self): + entry = p4runtime_pb2.PacketReplicationEngineEntry() + mcg_entry = entry.multicast_group_entry + mcg_entry.multicast_group_id = self.group_id + for replica in self.replicas: + r = mcg_entry.replicas.add() + r.CopyFrom(replica._msg) + self._entry = entry + + def add(self, egress_port=None, instance=0): + """Add a replica to the multicast group.""" + self.replicas.append(Replica(egress_port, instance)) + return self + + def _write(self, type_): + if self.group_id == 0: + raise UserError("0 is not a valid group_id for MulticastGroupEntry") + super()._write(type_) + + +class CloneSessionEntry(_EntityBase): + """ + P4 clone session entry. + """ + + def __init__(self, session_id=0): + super().__init__( + P4RuntimeEntity.packet_replication_engine_entry, + p4runtime_pb2.PacketReplicationEngineEntry) + self.session_id = session_id + self.replicas = [] + self.cos = 0 + self.packet_length_bytes = 0 + self.__doc__ = """ +Clone session entry. +Create an instance with clone_session_entry(<session_id>). +Add replicas with <self>.add(<eg_port_1>, <instance_1>).add(<eg_port_2>, +<instance_2>)... +Access class of service with <self>.cos. +Access truncation length with <self>.packet_length_bytes. +""" + self._init = True + + def __dir__(self): + return ["session_id", "replicas", "cos", "packet_length_bytes"] + + def __setattr__(self, name, value): + if name[0] == "_": + super().__setattr__(name, value) + return + if name == "session_id": + if not isinstance(value, int): + raise UserError("session_id must be an integer") + if name == "replicas": + if not isinstance(value, list): + raise UserError("replicas must be a list of Replica objects") + for r in value: + if not isinstance(r, Replica): + raise UserError( + "replicas must be a list of Replica objects") + if name == "cos": + if not isinstance(value, int): + raise UserError("cos must be an integer") + if name == "packet_length_bytes": + if not isinstance(value, int): + raise UserError("packet_length_bytes must be an integer") + super().__setattr__(name, value) + + def _from_msg(self, msg): + self.session_id = msg.clone_session_entry.session_id + for r in msg.clone_session_entry.replicas: + self.add(r.egress_port, r.instance) + self.cos = msg.clone_session_entry.class_of_service + self.packet_length_bytes = msg.clone_session_entry.packet_length_bytes + + def read(self, function=None): + """Generate a P4Runtime Read RPC. Supports wildcard reads (just leave + the session_id as 0). + If function is None, returns an iterator. Iterate over it to get all the + clone session entries (CloneSessionEntry instances) returned by the + server. Otherwise, function is applied to all the clone session entries + returned by the server. + + For example: + for c in <self>.read(): + print(c) + The above code is equivalent to the following one-liner: + <self>.read(lambda c: print(c)) + """ + return super().read(function) + + def _update_msg(self): + entry = p4runtime_pb2.PacketReplicationEngineEntry() + cs_entry = entry.clone_session_entry + cs_entry.session_id = self.session_id + for replica in self.replicas: + r = cs_entry.replicas.add() + r.CopyFrom(replica._msg) + cs_entry.class_of_service = self.cos + cs_entry.packet_length_bytes = self.packet_length_bytes + self._entry = entry + + def add(self, egress_port=None, instance=0): + """Add a replica to the clone session.""" + self.replicas.append(Replica(egress_port, instance)) + return self + + def _write(self, type_): + if self.session_id == 0: + raise UserError("0 is not a valid group_id for CloneSessionEntry") + super()._write(type_) + + +class PacketMetadata: + """ + P4 packet metadata. + """ + + def __init__(self, metadata_info_list): + self._md_info = OrderedDict() + self._md = OrderedDict() + # Initialize every metadata to zero value + for md in metadata_info_list: + self._md_info[md.name] = md + self._md[md.name] = self._parse_md('0', md) + self._set_docstring() + + def _set_docstring(self): + self.__doc__ = "Available metadata:\n\n" + for _, info in self._md_info.items(): + self.__doc__ += str(info) + self.__doc__ += """ +Set a metadata value with <self>.['<metadata_name>'] = '...' + +You may also use <self>.set(<md_name>='<value>') +""" + + def __dir__(self): + return ["clear"] + + def _get_md_info(self, name): + if name in self._md_info: + return self._md_info[name] + raise UserError(f"'{name}' is not a valid metadata name") + + def __getitem__(self, name): + _ = self._get_md_info(name) + print(self._md.get(name, "Unset")) + + def _parse_md(self, value, md_info): + if not isinstance(value, str): + raise UserError("Metadata value must be a string") + md = p4runtime_pb2.PacketMetadata() + md.metadata_id = md_info.id + md.value = encode(value.strip(), md_info.bitwidth) + return md + + def __setitem__(self, name, value): + md_info = self._get_md_info(name) + self._md[name] = self._parse_md(value, md_info) + + def _ipython_key_completions_(self): + return self._md_info.keys() + + def set(self, **kwargs): + """ + Set packet metadata parameters. + + :param kwargs: packet metadata parameter map + :return: void + """ + for name, value in kwargs.items(): + self[name] = value + + def clear(self): + """ + Clear packet metadata. + + :return: void + """ + self._md.clear() + + def values(self): + """ + Get packet metadata values. + + :return: list of packet metadata values + """ + return self._md.values() + + +class PacketIn(): + """ + P4 packet in. + """ + + def __init__(self): + ctrl_pkt_md = P4Objects(P4Type.controller_packet_metadata) + self.md_info_list = {} + if "packet_in" in ctrl_pkt_md: + self.p4_info = ctrl_pkt_md["packet_in"] + for md_info in self.p4_info.metadata: + self.md_info_list[md_info.name] = md_info + self.packet_in_queue = queue.Queue() + + def _packet_in_recv_func(packet_in_queue): + while True: + msg = CLIENT.get_stream_packet("packet", timeout=None) + if not msg: + break + packet_in_queue.put(msg) + + self.recv_t = Thread(target=_packet_in_recv_func, + args=(self.packet_in_queue,)) + self.recv_t.start() + + def sniff(self, function=None, timeout=None): + """ + Return an iterator of packet-in messages. + If the function is provided, we do not return an iterator; + instead we apply the function to every packet-in message. + + :param function: packet-in function + :param timeout: timeout in seconds + :return: list of packet-in messages + """ + msgs = [] + + if timeout is not None and timeout < 0: + raise ValueError("Timeout can't be a negative number.") + + if timeout is None: + while True: + try: + msgs.append(self.packet_in_queue.get(block=True)) + except KeyboardInterrupt: + # User sends a Ctrl+C -> breaking + break + + else: # timeout parameter is provided + deadline = time.time() + timeout + remaining_time = timeout + while remaining_time > 0: + try: + msgs.append(self.packet_in_queue.get(block=True, + timeout=remaining_time)) + remaining_time = deadline - time.time() + except KeyboardInterrupt: + # User sends an interrupt(e.g., Ctrl+C). + break + except queue.Empty: + # No item available on timeout. Exiting + break + + if function is None: + return iter(msgs) + for msg in msgs: + function(msg) + + def str(self): + """ + Packet-in metadata to string. + + :return: void + """ + for name, info in self.md_info_list.itmes(): + print(f"Packet-in metadata attribute '{name}':'{info}'") + + +class PacketOut: + """ + P4 packet out. + """ + + def __init__(self, payload=b'', **kwargs): + + self.p4_info = P4Objects(P4Type.controller_packet_metadata)[ + "packet_out"] + self._entry = None + self.payload = payload + self.metadata = PacketMetadata(self.p4_info.metadata) + if kwargs: + for key, value in kwargs.items(): + self.metadata[key] = value + + def _update_msg(self): + self._entry = p4runtime_pb2.PacketOut() + self._entry.payload = self.payload + self._entry.metadata.extend(self.metadata.values()) + + def __setattr__(self, name, value): + if name == "payload" and not isinstance(value, bytes): + raise UserError("payload must be a bytes type") + if name == "metadata" and not isinstance(value, PacketMetadata): + raise UserError("metadata must be a PacketMetadata type") + return super().__setattr__(name, value) + + def __dir__(self): + return ["metadata", "send", "payload"] + + def __str__(self): + self._update_msg() + return str(self._entry) + + def send(self): + """ + Send a packet-out message. + + :return: void + """ + self._update_msg() + msg = p4runtime_pb2.StreamMessageRequest() + msg.packet.CopyFrom(self._entry) + CLIENT.stream_out_q.put(msg) + + def str(self): + """ + Packet-out metadata to string. + + :return: void + """ + for key, value in self.metadata.itmes(): + print(f"Packet-out metadata attribute '{key}':'{value}'") + + +class IdleTimeoutNotification(): + """ + P4 idle timeout notification. + """ + + def __init__(self): + self.notification_queue = queue.Queue() + + def _notification_recv_func(notification_queue): + while True: + msg = CLIENT.get_stream_packet("idle_timeout_notification", + timeout=None) + if not msg: + break + notification_queue.put(msg) + + self.recv_t = Thread(target=_notification_recv_func, + args=(self.notification_queue,)) + self.recv_t.start() + + def sniff(self, function=None, timeout=None): + """ + Return an iterator of notification messages. + If the function is provided, we do not return an iterator and instead we apply + the function to every notification message. + """ + msgs = [] + + if timeout is not None and timeout < 0: + raise ValueError("Timeout can't be a negative number.") + + if timeout is None: + while True: + try: + msgs.append(self.notification_queue.get(block=True)) + except KeyboardInterrupt: + # User sends a Ctrl+C -> breaking + break + + else: # timeout parameter is provided + deadline = time.time() + timeout + remaining_time = timeout + while remaining_time > 0: + try: + msgs.append(self.notification_queue.get(block=True, + timeout=remaining_time)) + remaining_time = deadline - time.time() + except KeyboardInterrupt: + # User sends an interrupt(e.g., Ctrl+C). + break + except queue.Empty: + # No item available on timeout. Exiting + break + + if function is None: + return iter(msgs) + for msg in msgs: + function(msg) diff --git a/src/device/service/drivers/p4/p4_util.py b/src/device/service/drivers/p4/p4_util.py deleted file mode 100644 index b3d54499f56772768dc19bc1cae3bbf9a25e7dc2..0000000000000000000000000000000000000000 --- a/src/device/service/drivers/p4/p4_util.py +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -P4 driver utilities. -""" - -import logging -import queue -import sys -import threading -from functools import wraps -import grpc -import google.protobuf.text_format -from google.rpc import code_pb2 - -from p4.v1 import p4runtime_pb2 -from p4.v1 import p4runtime_pb2_grpc - -P4_ATTR_DEV_ID = 'id' -P4_ATTR_DEV_NAME = 'name' -P4_ATTR_DEV_VENDOR = 'vendor' -P4_ATTR_DEV_HW_VER = 'hw_ver' -P4_ATTR_DEV_SW_VER = 'sw_ver' -P4_ATTR_DEV_PIPECONF = 'pipeconf' - -P4_VAL_DEF_VENDOR = 'Unknown' -P4_VAL_DEF_HW_VER = 'BMv2 simple_switch' -P4_VAL_DEF_SW_VER = 'Stratum' -P4_VAL_DEF_PIPECONF = 'org.onosproject.pipelines.fabric' - -STREAM_ATTR_ARBITRATION = 'arbitration' -STREAM_ATTR_PACKET = 'packet' -STREAM_ATTR_DIGEST = 'digest' -STREAM_ATTR_UNKNOWN = 'unknown' - -LOGGER = logging.getLogger(__name__) - - -class P4RuntimeException(Exception): - """ - P4Runtime exception handler. - - Attributes - ---------- - grpc_error : object - gRPC error - """ - - def __init__(self, grpc_error): - super().__init__() - self.grpc_error = grpc_error - - def __str__(self): - return str('P4Runtime RPC error (%s): %s', - self.grpc_error.code().name(), self.grpc_error.details()) - - -def parse_p4runtime_error(fun): - """ - Parse P4Runtime error. - - :param fun: function - :return: parsed error - """ - @wraps(fun) - def handle(*args, **kwargs): - try: - return fun(*args, **kwargs) - except grpc.RpcError as rpc_ex: - raise P4RuntimeException(rpc_ex) from None - except Exception as ex: - raise Exception(ex) from None - return handle - - -class P4RuntimeClient: - """ - P4Runtime client. - - Attributes - ---------- - device_id : int - P4 device ID - grpc_address : str - IP address and port - election_id : tuple - Mastership election ID - role_name : str - Role name (optional) - """ - def __init__(self, device_id, grpc_address, election_id, role_name=None): - self.device_id = device_id - self.election_id = election_id - self.role_name = role_name - self.stream_in_q = None - self.stream_out_q = None - self.stream = None - self.stream_recv_thread = None - LOGGER.debug( - 'Connecting to device %d at %s', device_id, grpc_address) - self.channel = grpc.insecure_channel(grpc_address) - self.stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel) - try: - self.set_up_stream() - except P4RuntimeException: - LOGGER.critical('Failed to connect to P4Runtime server') - sys.exit(1) - - def set_up_stream(self): - """ - Set up a gRPC stream. - """ - self.stream_out_q = queue.Queue() - # queues for different messages - self.stream_in_q = { - STREAM_ATTR_ARBITRATION: queue.Queue(), - STREAM_ATTR_PACKET: queue.Queue(), - STREAM_ATTR_DIGEST: queue.Queue(), - STREAM_ATTR_UNKNOWN: queue.Queue(), - } - - def stream_req_iterator(): - while True: - st_p = self.stream_out_q.get() - if st_p is None: - break - yield st_p - - def stream_recv_wrapper(stream): - @parse_p4runtime_error - def stream_recv(): - for st_p in stream: - if st_p.HasField(STREAM_ATTR_ARBITRATION): - self.stream_in_q[STREAM_ATTR_ARBITRATION].put(st_p) - elif st_p.HasField(STREAM_ATTR_PACKET): - self.stream_in_q[STREAM_ATTR_PACKET].put(st_p) - elif st_p.HasField(STREAM_ATTR_DIGEST): - self.stream_in_q[STREAM_ATTR_DIGEST].put(st_p) - else: - self.stream_in_q[STREAM_ATTR_UNKNOWN].put(st_p) - try: - stream_recv() - except P4RuntimeException as ex: - LOGGER.critical('StreamChannel error, closing stream') - LOGGER.critical(ex) - for k in self.stream_in_q: - self.stream_in_q[k].put(None) - self.stream = self.stub.StreamChannel(stream_req_iterator()) - self.stream_recv_thread = threading.Thread( - target=stream_recv_wrapper, args=(self.stream,)) - self.stream_recv_thread.start() - self.handshake() - - def handshake(self): - """ - Handshake with gRPC server. - """ - - req = p4runtime_pb2.StreamMessageRequest() - arbitration = req.arbitration - arbitration.device_id = self.device_id - election_id = arbitration.election_id - election_id.high = self.election_id[0] - election_id.low = self.election_id[1] - if self.role_name is not None: - arbitration.role.name = self.role_name - self.stream_out_q.put(req) - - rep = self.get_stream_packet(STREAM_ATTR_ARBITRATION, timeout=2) - if rep is None: - LOGGER.critical('Failed to establish session with server') - sys.exit(1) - is_primary = (rep.arbitration.status.code == code_pb2.OK) - LOGGER.debug('Session established, client is %s', - 'primary' if is_primary else 'backup') - if not is_primary: - LOGGER.warning( - 'You are not the primary client, ' - 'you only have read access to the server') - - def get_stream_packet(self, type_, timeout=1): - """ - Get a new message from the stream. - - :param type_: stream type. - :param timeout: time to wait. - :return: message or None - """ - if type_ not in self.stream_in_q: - LOGGER.critical('Unknown stream type %s', type_) - return None - try: - msg = self.stream_in_q[type_].get(timeout=timeout) - return msg - except queue.Empty: # timeout expired - return None - - @parse_p4runtime_error - def get_p4info(self): - """ - Retrieve P4Info content. - - :return: P4Info object. - """ - - LOGGER.debug('Retrieving P4Info file') - req = p4runtime_pb2.GetForwardingPipelineConfigRequest() - req.device_id = self.device_id - req.response_type =\ - p4runtime_pb2.GetForwardingPipelineConfigRequest.P4INFO_AND_COOKIE - rep = self.stub.GetForwardingPipelineConfig(req) - return rep.config.p4info - - @parse_p4runtime_error - def set_fwd_pipe_config(self, p4info_path, bin_path): - """ - Configure the pipeline. - - :param p4info_path: path to the P4Info file - :param bin_path: path to the binary file - :return: - """ - - LOGGER.debug('Setting forwarding pipeline config') - req = p4runtime_pb2.SetForwardingPipelineConfigRequest() - req.device_id = self.device_id - if self.role_name is not None: - req.role = self.role_name - election_id = req.election_id - election_id.high = self.election_id[0] - election_id.low = self.election_id[1] - req.action =\ - p4runtime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_COMMIT - with open(p4info_path, 'r', encoding='utf8') as f_1: - with open(bin_path, 'rb', encoding='utf8') as f_2: - try: - google.protobuf.text_format.Merge( - f_1.read(), req.config.p4info) - except google.protobuf.text_format.ParseError: - LOGGER.error('Error when parsing P4Info') - raise - req.config.p4_device_config = f_2.read() - return self.stub.SetForwardingPipelineConfig(req) - - def tear_down(self): - """ - Tear connection with the gRPC server down. - """ - - if self.stream_out_q: - LOGGER.debug('Cleaning up stream') - self.stream_out_q.put(None) - if self.stream_in_q: - for k in self.stream_in_q: - self.stream_in_q[k].put(None) - if self.stream_recv_thread: - self.stream_recv_thread.join() - self.channel.close() - del self.channel diff --git a/src/device/tests/device_p4.py b/src/device/tests/device_p4.py index 4cd0a4c745d3a07b71f320ce79d73c95ffb0af37..ccc62c2195c8dae41a8e98b128b08965954d57f0 100644 --- a/src/device/tests/device_p4.py +++ b/src/device/tests/device_p4.py @@ -16,18 +16,23 @@ P4 device example configuration. """ -from common.tools.object_factory.ConfigRule import json_config_rule_set +import os +from common.tools.object_factory.ConfigRule import ( + json_config_rule_set, json_config_rule_delete) from common.tools.object_factory.Device import ( json_device_connect_rules, json_device_id, json_device_p4_disabled) -DEVICE_P4_DPID = 0 +CUR_PATH = os.path.dirname(os.path.abspath(__file__)) + +DEVICE_P4_DPID = 1 DEVICE_P4_NAME = 'device:leaf1' -DEVICE_P4_ADDRESS = '127.0.0.1' +DEVICE_P4_IP_ADDR = '127.0.0.1' DEVICE_P4_PORT = '50101' DEVICE_P4_VENDOR = 'Open Networking Foundation' DEVICE_P4_HW_VER = 'BMv2 simple_switch' DEVICE_P4_SW_VER = 'Stratum' -DEVICE_P4_PIPECONF = 'org.onosproject.pipelines.fabric' +DEVICE_P4_BIN_PATH = os.path.join(CUR_PATH, 'p4/test-bmv2.json') +DEVICE_P4_INFO_PATH = os.path.join(CUR_PATH, 'p4/test-p4info.txt') DEVICE_P4_WORKERS = 2 DEVICE_P4_GRACE_PERIOD = 60 DEVICE_P4_TIMEOUT = 60 @@ -37,16 +42,52 @@ DEVICE_P4_ID = json_device_id(DEVICE_P4_UUID) DEVICE_P4 = json_device_p4_disabled(DEVICE_P4_UUID) DEVICE_P4_CONNECT_RULES = json_device_connect_rules( - DEVICE_P4_ADDRESS, DEVICE_P4_PORT, { + DEVICE_P4_IP_ADDR, + DEVICE_P4_PORT, + { 'id': DEVICE_P4_DPID, 'name': DEVICE_P4_NAME, - 'hw-ver': DEVICE_P4_HW_VER, - 'sw-ver': DEVICE_P4_SW_VER, - 'pipeconf': DEVICE_P4_PIPECONF, - 'timeout': DEVICE_P4_TIMEOUT + 'vendor': DEVICE_P4_VENDOR, + 'hw_ver': DEVICE_P4_HW_VER, + 'sw_ver': DEVICE_P4_SW_VER, + 'timeout': DEVICE_P4_TIMEOUT, + 'p4bin': DEVICE_P4_BIN_PATH, + 'p4info': DEVICE_P4_INFO_PATH } ) -DEVICE_P4_CONFIG_RULES = [ - json_config_rule_set('key1', 'value1'), +DEVICE_P4_CONFIG_TABLE_ENTRY = [ + json_config_rule_set( + 'table', + { + 'table-name': 'IngressPipeImpl.acl_table', + 'match-fields': [ + { + 'match-field': 'hdr.ethernet.dst_addr', + 'match-value': 'aa:bb:cc:dd:ee:22 &&& ff:ff:ff:ff:ff:ff' + } + ], + 'action-name': 'IngressPipeImpl.clone_to_cpu', + 'action-params': [], + 'priority': 1 + } + ) +] + +DEVICE_P4_DECONFIG_TABLE_ENTRY = [ + json_config_rule_delete( + 'table', + { + 'table-name': 'IngressPipeImpl.acl_table', + 'match-fields': [ + { + 'match-field': 'hdr.ethernet.dst_addr', + 'match-value': 'aa:bb:cc:dd:ee:22 &&& ff:ff:ff:ff:ff:ff' + } + ], + 'action-name': 'IngressPipeImpl.clone_to_cpu', + 'action-params': [], + 'priority': 1 + } + ) ] diff --git a/src/device/tests/mock_p4runtime_service.py b/src/device/tests/mock_p4runtime_service.py index 77da0113676dc6f820d995b34915df6d0ba30f01..c1b2dcb45a18caf0c839f9bd8a68484bba5efbea 100644 --- a/src/device/tests/mock_p4runtime_service.py +++ b/src/device/tests/mock_p4runtime_service.py @@ -22,7 +22,7 @@ import grpc from p4.v1 import p4runtime_pb2_grpc from .device_p4 import( - DEVICE_P4_ADDRESS, DEVICE_P4_PORT, + DEVICE_P4_IP_ADDR, DEVICE_P4_PORT, DEVICE_P4_WORKERS, DEVICE_P4_GRACE_PERIOD) from .mock_p4runtime_servicer_impl import MockP4RuntimeServicerImpl @@ -35,7 +35,7 @@ class MockP4RuntimeService: """ def __init__( - self, address=DEVICE_P4_ADDRESS, port=DEVICE_P4_PORT, + self, address=DEVICE_P4_IP_ADDR, port=DEVICE_P4_PORT, max_workers=DEVICE_P4_WORKERS, grace_period=DEVICE_P4_GRACE_PERIOD): self.address = address diff --git a/src/device/tests/mock_p4runtime_servicer_impl.py b/src/device/tests/mock_p4runtime_servicer_impl.py index d29445da43afb58ef062f62c496b0780f92a4648..8a516303d9310be55662ef749175655c4069ae5c 100644 --- a/src/device/tests/mock_p4runtime_servicer_impl.py +++ b/src/device/tests/mock_p4runtime_servicer_impl.py @@ -22,11 +22,12 @@ from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc from p4.config.v1 import p4info_pb2 try: - from p4_util import STREAM_ATTR_ARBITRATION, STREAM_ATTR_PACKET + from p4_client import STREAM_ATTR_ARBITRATION, STREAM_ATTR_PACKET except ImportError: - from device.service.drivers.p4.p4_util import STREAM_ATTR_ARBITRATION,\ + from device.service.drivers.p4.p4_client import STREAM_ATTR_ARBITRATION,\ STREAM_ATTR_PACKET + class MockP4RuntimeServicerImpl(p4runtime_pb2_grpc.P4RuntimeServicer): """ A P4Runtime service implementation for testing purposes. diff --git a/src/device/tests/p4/test-bmv2.json b/src/device/tests/p4/test-bmv2.json new file mode 100644 index 0000000000000000000000000000000000000000..f6ef6af34907ae00bcfa1034bb317a8f270b8995 --- /dev/null +++ b/src/device/tests/p4/test-bmv2.json @@ -0,0 +1,1910 @@ +{ + "header_types" : [ + { + "name" : "scalars_0", + "id" : 0, + "fields" : [ + ["tmp_0", 1, false], + ["tmp_1", 1, false], + ["tmp", 1, false], + ["local_metadata_t.l4_src_port", 16, false], + ["local_metadata_t.l4_dst_port", 16, false], + ["local_metadata_t.is_multicast", 1, false], + ["local_metadata_t.next_srv6_sid", 128, false], + ["local_metadata_t.ip_proto", 8, false], + ["local_metadata_t.icmp_type", 8, false], + ["_padding_0", 4, false] + ] + }, + { + "name" : "standard_metadata", + "id" : 1, + "fields" : [ + ["ingress_port", 9, false], + ["egress_spec", 9, false], + ["egress_port", 9, false], + ["clone_spec", 32, false], + ["instance_type", 32, false], + ["drop", 1, false], + ["recirculate_port", 16, false], + ["packet_length", 32, false], + ["enq_timestamp", 32, false], + ["enq_qdepth", 19, false], + ["deq_timedelta", 32, false], + ["deq_qdepth", 19, false], + ["ingress_global_timestamp", 48, false], + ["egress_global_timestamp", 48, false], + ["lf_field_list", 32, false], + ["mcast_grp", 16, false], + ["resubmit_flag", 32, false], + ["egress_rid", 16, false], + ["recirculate_flag", 32, false], + ["checksum_error", 1, false], + ["parser_error", 32, false], + ["priority", 3, false], + ["_padding", 2, false] + ] + }, + { + "name" : "cpu_out_header_t", + "id" : 2, + "fields" : [ + ["egress_port", 9, false], + ["_pad", 7, false] + ] + }, + { + "name" : "cpu_in_header_t", + "id" : 3, + "fields" : [ + ["ingress_port", 9, false], + ["_pad", 7, false] + ] + }, + { + "name" : "ethernet_t", + "id" : 4, + "fields" : [ + ["dst_addr", 48, false], + ["src_addr", 48, false], + ["ether_type", 16, false] + ] + }, + { + "name" : "ipv4_t", + "id" : 5, + "fields" : [ + ["version", 4, false], + ["ihl", 4, false], + ["dscp", 6, false], + ["ecn", 2, false], + ["total_len", 16, false], + ["identification", 16, false], + ["flags", 3, false], + ["frag_offset", 13, false], + ["ttl", 8, false], + ["protocol", 8, false], + ["hdr_checksum", 16, false], + ["src_addr", 32, false], + ["dst_addr", 32, false] + ] + }, + { + "name" : "ipv6_t", + "id" : 6, + "fields" : [ + ["version", 4, false], + ["traffic_class", 8, false], + ["flow_label", 20, false], + ["payload_len", 16, false], + ["next_hdr", 8, false], + ["hop_limit", 8, false], + ["src_addr", 128, false], + ["dst_addr", 128, false] + ] + }, + { + "name" : "srv6h_t", + "id" : 7, + "fields" : [ + ["next_hdr", 8, false], + ["hdr_ext_len", 8, false], + ["routing_type", 8, false], + ["segment_left", 8, false], + ["last_entry", 8, false], + ["flags", 8, false], + ["tag", 16, false] + ] + }, + { + "name" : "tcp_t", + "id" : 8, + "fields" : [ + ["src_port", 16, false], + ["dst_port", 16, false], + ["seq_no", 32, false], + ["ack_no", 32, false], + ["data_offset", 4, false], + ["res", 3, false], + ["ecn", 3, false], + ["ctrl", 6, false], + ["window", 16, false], + ["checksum", 16, false], + ["urgent_ptr", 16, false] + ] + }, + { + "name" : "udp_t", + "id" : 9, + "fields" : [ + ["src_port", 16, false], + ["dst_port", 16, false], + ["len", 16, false], + ["checksum", 16, false] + ] + }, + { + "name" : "icmp_t", + "id" : 10, + "fields" : [ + ["type", 8, false], + ["icmp_code", 8, false], + ["checksum", 16, false], + ["identifier", 16, false], + ["sequence_number", 16, false], + ["timestamp", 64, false] + ] + }, + { + "name" : "icmpv6_t", + "id" : 11, + "fields" : [ + ["type", 8, false], + ["code", 8, false], + ["checksum", 16, false] + ] + }, + { + "name" : "ndp_t", + "id" : 12, + "fields" : [ + ["flags", 32, false], + ["target_ipv6_addr", 128, false], + ["type", 8, false], + ["length", 8, false], + ["target_mac_addr", 48, false] + ] + }, + { + "name" : "srv6_list_t", + "id" : 13, + "fields" : [ + ["segment_id", 128, false] + ] + } + ], + "headers" : [ + { + "name" : "scalars", + "id" : 0, + "header_type" : "scalars_0", + "metadata" : true, + "pi_omit" : true + }, + { + "name" : "standard_metadata", + "id" : 1, + "header_type" : "standard_metadata", + "metadata" : true, + "pi_omit" : true + }, + { + "name" : "cpu_out", + "id" : 2, + "header_type" : "cpu_out_header_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "cpu_in", + "id" : 3, + "header_type" : "cpu_in_header_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "ethernet", + "id" : 4, + "header_type" : "ethernet_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "ipv4", + "id" : 5, + "header_type" : "ipv4_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "ipv6", + "id" : 6, + "header_type" : "ipv6_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "srv6h", + "id" : 7, + "header_type" : "srv6h_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "tcp", + "id" : 8, + "header_type" : "tcp_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "udp", + "id" : 9, + "header_type" : "udp_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "icmp", + "id" : 10, + "header_type" : "icmp_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "icmpv6", + "id" : 11, + "header_type" : "icmpv6_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "ndp", + "id" : 12, + "header_type" : "ndp_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "srv6_list[0]", + "id" : 13, + "header_type" : "srv6_list_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "srv6_list[1]", + "id" : 14, + "header_type" : "srv6_list_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "srv6_list[2]", + "id" : 15, + "header_type" : "srv6_list_t", + "metadata" : false, + "pi_omit" : true + }, + { + "name" : "srv6_list[3]", + "id" : 16, + "header_type" : "srv6_list_t", + "metadata" : false, + "pi_omit" : true + } + ], + "header_stacks" : [ + { + "name" : "srv6_list", + "id" : 0, + "header_type" : "srv6_list_t", + "size" : 4, + "header_ids" : [13, 14, 15, 16] + } + ], + "header_union_types" : [], + "header_unions" : [], + "header_union_stacks" : [], + "field_lists" : [ + { + "id" : 1, + "name" : "fl", + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 474, + "column" : 34, + "source_fragment" : "{ standard_metadata.ingress_port }" + }, + "elements" : [ + { + "type" : "field", + "value" : ["standard_metadata", "ingress_port"] + } + ] + } + ], + "errors" : [ + ["NoError", 1], + ["PacketTooShort", 2], + ["NoMatch", 3], + ["StackOutOfBounds", 4], + ["HeaderTooShort", 5], + ["ParserTimeout", 6], + ["ParserInvalidArgument", 7] + ], + "enums" : [], + "parsers" : [ + { + "name" : "parser", + "id" : 0, + "init_state" : "start", + "parse_states" : [ + { + "name" : "start", + "id" : 0, + "parser_ops" : [], + "transitions" : [ + { + "type" : "hexstr", + "value" : "0x00ff", + "mask" : null, + "next_state" : "parse_packet_out" + }, + { + "value" : "default", + "mask" : null, + "next_state" : "parse_ethernet" + } + ], + "transition_key" : [ + { + "type" : "field", + "value" : ["standard_metadata", "ingress_port"] + } + ] + }, + { + "name" : "parse_packet_out", + "id" : 1, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "regular", + "value" : "cpu_out" + } + ], + "op" : "extract" + } + ], + "transitions" : [ + { + "value" : "default", + "mask" : null, + "next_state" : "parse_ethernet" + } + ], + "transition_key" : [] + }, + { + "name" : "parse_ethernet", + "id" : 2, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "regular", + "value" : "ethernet" + } + ], + "op" : "extract" + } + ], + "transitions" : [ + { + "type" : "hexstr", + "value" : "0x0800", + "mask" : null, + "next_state" : "parse_ipv4" + }, + { + "type" : "hexstr", + "value" : "0x86dd", + "mask" : null, + "next_state" : "parse_ipv6" + }, + { + "value" : "default", + "mask" : null, + "next_state" : null + } + ], + "transition_key" : [ + { + "type" : "field", + "value" : ["ethernet", "ether_type"] + } + ] + }, + { + "name" : "parse_ipv4", + "id" : 3, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "regular", + "value" : "ipv4" + } + ], + "op" : "extract" + }, + { + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "local_metadata_t.ip_proto"] + }, + { + "type" : "field", + "value" : ["ipv4", "protocol"] + } + ], + "op" : "set" + } + ], + "transitions" : [ + { + "type" : "hexstr", + "value" : "0x06", + "mask" : null, + "next_state" : "parse_tcp" + }, + { + "type" : "hexstr", + "value" : "0x11", + "mask" : null, + "next_state" : "parse_udp" + }, + { + "type" : "hexstr", + "value" : "0x01", + "mask" : null, + "next_state" : "parse_icmp" + }, + { + "value" : "default", + "mask" : null, + "next_state" : null + } + ], + "transition_key" : [ + { + "type" : "field", + "value" : ["ipv4", "protocol"] + } + ] + }, + { + "name" : "parse_ipv6", + "id" : 4, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "regular", + "value" : "ipv6" + } + ], + "op" : "extract" + }, + { + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "local_metadata_t.ip_proto"] + }, + { + "type" : "field", + "value" : ["ipv6", "next_hdr"] + } + ], + "op" : "set" + } + ], + "transitions" : [ + { + "type" : "hexstr", + "value" : "0x06", + "mask" : null, + "next_state" : "parse_tcp" + }, + { + "type" : "hexstr", + "value" : "0x11", + "mask" : null, + "next_state" : "parse_udp" + }, + { + "type" : "hexstr", + "value" : "0x3a", + "mask" : null, + "next_state" : "parse_icmpv6" + }, + { + "type" : "hexstr", + "value" : "0x2b", + "mask" : null, + "next_state" : "parse_srv6" + }, + { + "value" : "default", + "mask" : null, + "next_state" : null + } + ], + "transition_key" : [ + { + "type" : "field", + "value" : ["ipv6", "next_hdr"] + } + ] + }, + { + "name" : "parse_tcp", + "id" : 5, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "regular", + "value" : "tcp" + } + ], + "op" : "extract" + }, + { + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "local_metadata_t.l4_src_port"] + }, + { + "type" : "field", + "value" : ["tcp", "src_port"] + } + ], + "op" : "set" + }, + { + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "local_metadata_t.l4_dst_port"] + }, + { + "type" : "field", + "value" : ["tcp", "dst_port"] + } + ], + "op" : "set" + } + ], + "transitions" : [ + { + "value" : "default", + "mask" : null, + "next_state" : null + } + ], + "transition_key" : [] + }, + { + "name" : "parse_udp", + "id" : 6, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "regular", + "value" : "udp" + } + ], + "op" : "extract" + }, + { + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "local_metadata_t.l4_src_port"] + }, + { + "type" : "field", + "value" : ["udp", "src_port"] + } + ], + "op" : "set" + }, + { + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "local_metadata_t.l4_dst_port"] + }, + { + "type" : "field", + "value" : ["udp", "dst_port"] + } + ], + "op" : "set" + } + ], + "transitions" : [ + { + "value" : "default", + "mask" : null, + "next_state" : null + } + ], + "transition_key" : [] + }, + { + "name" : "parse_icmp", + "id" : 7, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "regular", + "value" : "icmp" + } + ], + "op" : "extract" + }, + { + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "local_metadata_t.icmp_type"] + }, + { + "type" : "field", + "value" : ["icmp", "type"] + } + ], + "op" : "set" + } + ], + "transitions" : [ + { + "value" : "default", + "mask" : null, + "next_state" : null + } + ], + "transition_key" : [] + }, + { + "name" : "parse_icmpv6", + "id" : 8, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "regular", + "value" : "icmpv6" + } + ], + "op" : "extract" + }, + { + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "local_metadata_t.icmp_type"] + }, + { + "type" : "field", + "value" : ["icmpv6", "type"] + } + ], + "op" : "set" + } + ], + "transitions" : [ + { + "type" : "hexstr", + "value" : "0x87", + "mask" : null, + "next_state" : "parse_ndp" + }, + { + "type" : "hexstr", + "value" : "0x88", + "mask" : null, + "next_state" : "parse_ndp" + }, + { + "value" : "default", + "mask" : null, + "next_state" : null + } + ], + "transition_key" : [ + { + "type" : "field", + "value" : ["icmpv6", "type"] + } + ] + }, + { + "name" : "parse_ndp", + "id" : 9, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "regular", + "value" : "ndp" + } + ], + "op" : "extract" + } + ], + "transitions" : [ + { + "value" : "default", + "mask" : null, + "next_state" : null + } + ], + "transition_key" : [] + }, + { + "name" : "parse_srv6", + "id" : 10, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "regular", + "value" : "srv6h" + } + ], + "op" : "extract" + } + ], + "transitions" : [ + { + "value" : "default", + "mask" : null, + "next_state" : "parse_srv6_list" + } + ], + "transition_key" : [] + }, + { + "name" : "parse_srv6_list", + "id" : 11, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "stack", + "value" : "srv6_list" + } + ], + "op" : "extract" + }, + { + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "tmp_0"] + }, + { + "type" : "expression", + "value" : { + "type" : "expression", + "value" : { + "op" : "?", + "left" : { + "type" : "hexstr", + "value" : "0x01" + }, + "right" : { + "type" : "hexstr", + "value" : "0x00" + }, + "cond" : { + "type" : "expression", + "value" : { + "op" : "==", + "left" : { + "type" : "expression", + "value" : { + "op" : "&", + "left" : { + "type" : "expression", + "value" : { + "op" : "+", + "left" : { + "type" : "expression", + "value" : { + "op" : "&", + "left" : { + "type" : "field", + "value" : ["srv6h", "segment_left"] + }, + "right" : { + "type" : "hexstr", + "value" : "0xffffffff" + } + } + }, + "right" : { + "type" : "hexstr", + "value" : "0xffffffff" + } + } + }, + "right" : { + "type" : "hexstr", + "value" : "0xffffffff" + } + } + }, + "right" : { + "type" : "expression", + "value" : { + "op" : "last_stack_index", + "left" : null, + "right" : { + "type" : "header_stack", + "value" : "srv6_list" + } + } + } + } + } + } + } + } + ], + "op" : "set" + } + ], + "transitions" : [ + { + "type" : "hexstr", + "value" : "0x01", + "mask" : null, + "next_state" : "mark_current_srv6" + }, + { + "value" : "default", + "mask" : null, + "next_state" : "check_last_srv6" + } + ], + "transition_key" : [ + { + "type" : "field", + "value" : ["scalars", "tmp_0"] + } + ] + }, + { + "name" : "mark_current_srv6", + "id" : 12, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "local_metadata_t.next_srv6_sid"] + }, + { + "type" : "expression", + "value" : { + "type" : "stack_field", + "value" : ["srv6_list", "segment_id"] + } + } + ], + "op" : "set" + } + ], + "transitions" : [ + { + "value" : "default", + "mask" : null, + "next_state" : "check_last_srv6" + } + ], + "transition_key" : [] + }, + { + "name" : "check_last_srv6", + "id" : 13, + "parser_ops" : [ + { + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "tmp_1"] + }, + { + "type" : "expression", + "value" : { + "type" : "expression", + "value" : { + "op" : "?", + "left" : { + "type" : "hexstr", + "value" : "0x01" + }, + "right" : { + "type" : "hexstr", + "value" : "0x00" + }, + "cond" : { + "type" : "expression", + "value" : { + "op" : "==", + "left" : { + "type" : "expression", + "value" : { + "op" : "&", + "left" : { + "type" : "field", + "value" : ["srv6h", "last_entry"] + }, + "right" : { + "type" : "hexstr", + "value" : "0xffffffff" + } + } + }, + "right" : { + "type" : "expression", + "value" : { + "op" : "last_stack_index", + "left" : null, + "right" : { + "type" : "header_stack", + "value" : "srv6_list" + } + } + } + } + } + } + } + } + ], + "op" : "set" + } + ], + "transitions" : [ + { + "type" : "hexstr", + "value" : "0x01", + "mask" : null, + "next_state" : "parse_srv6_next_hdr" + }, + { + "type" : "hexstr", + "value" : "0x00", + "mask" : null, + "next_state" : "parse_srv6_list" + } + ], + "transition_key" : [ + { + "type" : "field", + "value" : ["scalars", "tmp_1"] + } + ] + }, + { + "name" : "parse_srv6_next_hdr", + "id" : 14, + "parser_ops" : [], + "transitions" : [ + { + "type" : "hexstr", + "value" : "0x06", + "mask" : null, + "next_state" : "parse_tcp" + }, + { + "type" : "hexstr", + "value" : "0x11", + "mask" : null, + "next_state" : "parse_udp" + }, + { + "type" : "hexstr", + "value" : "0x3a", + "mask" : null, + "next_state" : "parse_icmpv6" + }, + { + "value" : "default", + "mask" : null, + "next_state" : null + } + ], + "transition_key" : [ + { + "type" : "field", + "value" : ["srv6h", "next_hdr"] + } + ] + } + ] + } + ], + "parse_vsets" : [], + "deparsers" : [ + { + "name" : "deparser", + "id" : 0, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 602, + "column" : 8, + "source_fragment" : "DeparserImpl" + }, + "order" : ["cpu_in", "ethernet", "ipv4", "ipv6", "srv6h", "srv6_list[0]", "srv6_list[1]", "srv6_list[2]", "srv6_list[3]", "tcp", "udp", "icmp", "icmpv6", "ndp"] + } + ], + "meter_arrays" : [], + "counter_arrays" : [ + { + "name" : "l2_exact_table_counter", + "id" : 0, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 399, + "column" : 8, + "source_fragment" : "counters" + }, + "is_direct" : true, + "binding" : "IngressPipeImpl.l2_exact_table" + }, + { + "name" : "l2_ternary_table_counter", + "id" : 1, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 423, + "column" : 8, + "source_fragment" : "counters" + }, + "is_direct" : true, + "binding" : "IngressPipeImpl.l2_ternary_table" + }, + { + "name" : "acl_table_counter", + "id" : 2, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 494, + "column" : 8, + "source_fragment" : "counters" + }, + "is_direct" : true, + "binding" : "IngressPipeImpl.acl_table" + } + ], + "register_arrays" : [], + "calculations" : [ + { + "name" : "calc", + "id" : 0, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 580, + "column" : 8, + "source_fragment" : "update_checksum(hdr.ndp.isValid(), ..." + }, + "algo" : "csum16", + "input" : [ + { + "type" : "field", + "value" : ["ipv6", "src_addr"] + }, + { + "type" : "field", + "value" : ["ipv6", "dst_addr"] + }, + { + "type" : "field", + "value" : ["ipv6", "payload_len"] + }, + { + "type" : "hexstr", + "value" : "0x00", + "bitwidth" : 8 + }, + { + "type" : "field", + "value" : ["ipv6", "next_hdr"] + }, + { + "type" : "field", + "value" : ["icmpv6", "type"] + }, + { + "type" : "field", + "value" : ["icmpv6", "code"] + }, + { + "type" : "field", + "value" : ["ndp", "flags"] + }, + { + "type" : "field", + "value" : ["ndp", "target_ipv6_addr"] + }, + { + "type" : "field", + "value" : ["ndp", "type"] + }, + { + "type" : "field", + "value" : ["ndp", "length"] + }, + { + "type" : "field", + "value" : ["ndp", "target_mac_addr"] + } + ] + } + ], + "learn_lists" : [], + "actions" : [ + { + "name" : "NoAction", + "id" : 0, + "runtime_data" : [], + "primitives" : [] + }, + { + "name" : "IngressPipeImpl.drop", + "id" : 1, + "runtime_data" : [], + "primitives" : [ + { + "op" : "mark_to_drop", + "parameters" : [ + { + "type" : "header", + "value" : "standard_metadata" + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 351, + "column" : 8, + "source_fragment" : "mark_to_drop(standard_metadata)" + } + } + ] + }, + { + "name" : "IngressPipeImpl.drop", + "id" : 2, + "runtime_data" : [], + "primitives" : [ + { + "op" : "mark_to_drop", + "parameters" : [ + { + "type" : "header", + "value" : "standard_metadata" + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 351, + "column" : 8, + "source_fragment" : "mark_to_drop(standard_metadata)" + } + } + ] + }, + { + "name" : "IngressPipeImpl.drop", + "id" : 3, + "runtime_data" : [], + "primitives" : [ + { + "op" : "mark_to_drop", + "parameters" : [ + { + "type" : "header", + "value" : "standard_metadata" + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 351, + "column" : 8, + "source_fragment" : "mark_to_drop(standard_metadata)" + } + } + ] + }, + { + "name" : "IngressPipeImpl.set_egress_port", + "id" : 4, + "runtime_data" : [ + { + "name" : "port_num", + "bitwidth" : 9 + } + ], + "primitives" : [ + { + "op" : "assign", + "parameters" : [ + { + "type" : "field", + "value" : ["standard_metadata", "egress_spec"] + }, + { + "type" : "runtime_data", + "value" : 0 + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 383, + "column" : 8, + "source_fragment" : "standard_metadata.egress_spec = port_num" + } + } + ] + }, + { + "name" : "IngressPipeImpl.set_multicast_group", + "id" : 5, + "runtime_data" : [ + { + "name" : "gid", + "bitwidth" : 16 + } + ], + "primitives" : [ + { + "op" : "assign", + "parameters" : [ + { + "type" : "field", + "value" : ["standard_metadata", "mcast_grp"] + }, + { + "type" : "runtime_data", + "value" : 0 + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 409, + "column" : 8, + "source_fragment" : "standard_metadata.mcast_grp = gid" + } + }, + { + "op" : "assign", + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "local_metadata_t.is_multicast"] + }, + { + "type" : "expression", + "value" : { + "type" : "expression", + "value" : { + "op" : "b2d", + "left" : null, + "right" : { + "type" : "bool", + "value" : true + } + } + } + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 410, + "column" : 8, + "source_fragment" : "local_metadata.is_multicast = true" + } + } + ] + }, + { + "name" : "IngressPipeImpl.send_to_cpu", + "id" : 6, + "runtime_data" : [], + "primitives" : [ + { + "op" : "assign", + "parameters" : [ + { + "type" : "field", + "value" : ["standard_metadata", "egress_spec"] + }, + { + "type" : "hexstr", + "value" : "0x00ff" + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 466, + "column" : 8, + "source_fragment" : "standard_metadata.egress_spec = 255" + } + } + ] + }, + { + "name" : "IngressPipeImpl.clone_to_cpu", + "id" : 7, + "runtime_data" : [], + "primitives" : [ + { + "op" : "clone_ingress_pkt_to_egress", + "parameters" : [ + { + "type" : "hexstr", + "value" : "0x00000063" + }, + { + "type" : "hexstr", + "value" : "0x1" + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 474, + "column" : 8, + "source_fragment" : "clone3(CloneType.I2E, 99, { standard_metadata.ingress_port })" + } + } + ] + }, + { + "name" : "act", + "id" : 8, + "runtime_data" : [], + "primitives" : [ + { + "op" : "assign", + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "tmp"] + }, + { + "type" : "expression", + "value" : { + "type" : "expression", + "value" : { + "op" : "b2d", + "left" : null, + "right" : { + "type" : "bool", + "value" : true + } + } + } + } + ] + } + ] + }, + { + "name" : "act_0", + "id" : 9, + "runtime_data" : [], + "primitives" : [ + { + "op" : "assign", + "parameters" : [ + { + "type" : "field", + "value" : ["scalars", "tmp"] + }, + { + "type" : "expression", + "value" : { + "type" : "expression", + "value" : { + "op" : "b2d", + "left" : null, + "right" : { + "type" : "bool", + "value" : false + } + } + } + } + ] + } + ] + }, + { + "name" : "act_1", + "id" : 10, + "runtime_data" : [], + "primitives" : [ + { + "op" : "mark_to_drop", + "parameters" : [ + { + "type" : "header", + "value" : "standard_metadata" + } + ], + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 567, + "column" : 12, + "source_fragment" : "mark_to_drop(standard_metadata)" + } + } + ] + } + ], + "pipelines" : [ + { + "name" : "ingress", + "id" : 0, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 345, + "column" : 8, + "source_fragment" : "IngressPipeImpl" + }, + "init_table" : "IngressPipeImpl.l2_exact_table", + "tables" : [ + { + "name" : "IngressPipeImpl.l2_exact_table", + "id" : 0, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 386, + "column" : 10, + "source_fragment" : "l2_exact_table" + }, + "key" : [ + { + "match_type" : "exact", + "name" : "hdr.ethernet.dst_addr", + "target" : ["ethernet", "dst_addr"], + "mask" : null + } + ], + "match_type" : "exact", + "type" : "simple", + "max_size" : 1024, + "with_counters" : true, + "support_timeout" : false, + "direct_meters" : null, + "action_ids" : [4, 1], + "actions" : ["IngressPipeImpl.set_egress_port", "IngressPipeImpl.drop"], + "base_default_next" : null, + "next_tables" : { + "__HIT__" : "tbl_act", + "__MISS__" : "tbl_act_0" + }, + "default_entry" : { + "action_id" : 1, + "action_const" : true, + "action_data" : [], + "action_entry_const" : true + } + }, + { + "name" : "tbl_act", + "id" : 1, + "key" : [], + "match_type" : "exact", + "type" : "simple", + "max_size" : 1024, + "with_counters" : false, + "support_timeout" : false, + "direct_meters" : null, + "action_ids" : [8], + "actions" : ["act"], + "base_default_next" : "node_5", + "next_tables" : { + "act" : "node_5" + }, + "default_entry" : { + "action_id" : 8, + "action_const" : true, + "action_data" : [], + "action_entry_const" : true + } + }, + { + "name" : "tbl_act_0", + "id" : 2, + "key" : [], + "match_type" : "exact", + "type" : "simple", + "max_size" : 1024, + "with_counters" : false, + "support_timeout" : false, + "direct_meters" : null, + "action_ids" : [9], + "actions" : ["act_0"], + "base_default_next" : "node_5", + "next_tables" : { + "act_0" : "node_5" + }, + "default_entry" : { + "action_id" : 9, + "action_const" : true, + "action_data" : [], + "action_entry_const" : true + } + }, + { + "name" : "IngressPipeImpl.l2_ternary_table", + "id" : 3, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 413, + "column" : 10, + "source_fragment" : "l2_ternary_table" + }, + "key" : [ + { + "match_type" : "ternary", + "name" : "hdr.ethernet.dst_addr", + "target" : ["ethernet", "dst_addr"], + "mask" : null + } + ], + "match_type" : "ternary", + "type" : "simple", + "max_size" : 1024, + "with_counters" : true, + "support_timeout" : false, + "direct_meters" : null, + "action_ids" : [5, 2], + "actions" : ["IngressPipeImpl.set_multicast_group", "IngressPipeImpl.drop"], + "base_default_next" : "IngressPipeImpl.acl_table", + "next_tables" : { + "IngressPipeImpl.set_multicast_group" : "IngressPipeImpl.acl_table", + "IngressPipeImpl.drop" : "IngressPipeImpl.acl_table" + }, + "default_entry" : { + "action_id" : 2, + "action_const" : true, + "action_data" : [], + "action_entry_const" : true + } + }, + { + "name" : "IngressPipeImpl.acl_table", + "id" : 4, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 477, + "column" : 10, + "source_fragment" : "acl_table" + }, + "key" : [ + { + "match_type" : "ternary", + "name" : "standard_metadata.ingress_port", + "target" : ["standard_metadata", "ingress_port"], + "mask" : null + }, + { + "match_type" : "ternary", + "name" : "hdr.ethernet.dst_addr", + "target" : ["ethernet", "dst_addr"], + "mask" : null + }, + { + "match_type" : "ternary", + "name" : "hdr.ethernet.src_addr", + "target" : ["ethernet", "src_addr"], + "mask" : null + }, + { + "match_type" : "ternary", + "name" : "hdr.ethernet.ether_type", + "target" : ["ethernet", "ether_type"], + "mask" : null + }, + { + "match_type" : "ternary", + "name" : "local_metadata.ip_proto", + "target" : ["scalars", "local_metadata_t.ip_proto"], + "mask" : null + }, + { + "match_type" : "ternary", + "name" : "local_metadata.icmp_type", + "target" : ["scalars", "local_metadata_t.icmp_type"], + "mask" : null + }, + { + "match_type" : "ternary", + "name" : "local_metadata.l4_src_port", + "target" : ["scalars", "local_metadata_t.l4_src_port"], + "mask" : null + }, + { + "match_type" : "ternary", + "name" : "local_metadata.l4_dst_port", + "target" : ["scalars", "local_metadata_t.l4_dst_port"], + "mask" : null + } + ], + "match_type" : "ternary", + "type" : "simple", + "max_size" : 1024, + "with_counters" : true, + "support_timeout" : false, + "direct_meters" : null, + "action_ids" : [6, 7, 3, 0], + "actions" : ["IngressPipeImpl.send_to_cpu", "IngressPipeImpl.clone_to_cpu", "IngressPipeImpl.drop", "NoAction"], + "base_default_next" : null, + "next_tables" : { + "IngressPipeImpl.send_to_cpu" : null, + "IngressPipeImpl.clone_to_cpu" : null, + "IngressPipeImpl.drop" : null, + "NoAction" : null + }, + "default_entry" : { + "action_id" : 0, + "action_const" : false, + "action_data" : [], + "action_entry_const" : false + } + } + ], + "action_profiles" : [], + "conditionals" : [ + { + "name" : "node_5", + "id" : 0, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 533, + "column" : 16, + "source_fragment" : "!l2_exact_table.apply().hit" + }, + "expression" : { + "type" : "expression", + "value" : { + "op" : "not", + "left" : null, + "right" : { + "type" : "expression", + "value" : { + "op" : "d2b", + "left" : null, + "right" : { + "type" : "field", + "value" : ["scalars", "tmp"] + } + } + } + } + }, + "true_next" : "IngressPipeImpl.l2_ternary_table", + "false_next" : "IngressPipeImpl.acl_table" + } + ] + }, + { + "name" : "egress", + "id" : 1, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 546, + "column" : 8, + "source_fragment" : "EgressPipeImpl" + }, + "init_table" : "node_10", + "tables" : [ + { + "name" : "tbl_act_1", + "id" : 5, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 567, + "column" : 12, + "source_fragment" : "mark_to_drop(standard_metadata)" + }, + "key" : [], + "match_type" : "exact", + "type" : "simple", + "max_size" : 1024, + "with_counters" : false, + "support_timeout" : false, + "direct_meters" : null, + "action_ids" : [10], + "actions" : ["act_1"], + "base_default_next" : null, + "next_tables" : { + "act_1" : null + }, + "default_entry" : { + "action_id" : 10, + "action_const" : true, + "action_data" : [], + "action_entry_const" : true + } + } + ], + "action_profiles" : [], + "conditionals" : [ + { + "name" : "node_10", + "id" : 1, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 565, + "column" : 12, + "source_fragment" : "local_metadata.is_multicast == true && ..." + }, + "expression" : { + "type" : "expression", + "value" : { + "op" : "and", + "left" : { + "type" : "expression", + "value" : { + "op" : "==", + "left" : { + "type" : "expression", + "value" : { + "op" : "d2b", + "left" : null, + "right" : { + "type" : "field", + "value" : ["scalars", "local_metadata_t.is_multicast"] + } + } + }, + "right" : { + "type" : "bool", + "value" : true + } + } + }, + "right" : { + "type" : "expression", + "value" : { + "op" : "==", + "left" : { + "type" : "field", + "value" : ["standard_metadata", "ingress_port"] + }, + "right" : { + "type" : "field", + "value" : ["standard_metadata", "egress_port"] + } + } + } + } + }, + "false_next" : null, + "true_next" : "tbl_act_1" + } + ] + } + ], + "checksums" : [ + { + "name" : "cksum", + "id" : 0, + "source_info" : { + "filename" : "p4src/main.p4", + "line" : 580, + "column" : 8, + "source_fragment" : "update_checksum(hdr.ndp.isValid(), ..." + }, + "target" : ["icmpv6", "checksum"], + "type" : "generic", + "calculation" : "calc", + "verify" : false, + "update" : true, + "if_cond" : { + "type" : "expression", + "value" : { + "op" : "d2b", + "left" : null, + "right" : { + "type" : "field", + "value" : ["ndp", "$valid$"] + } + } + } + } + ], + "force_arith" : [], + "extern_instances" : [], + "field_aliases" : [ + [ + "queueing_metadata.enq_timestamp", + ["standard_metadata", "enq_timestamp"] + ], + [ + "queueing_metadata.enq_qdepth", + ["standard_metadata", "enq_qdepth"] + ], + [ + "queueing_metadata.deq_timedelta", + ["standard_metadata", "deq_timedelta"] + ], + [ + "queueing_metadata.deq_qdepth", + ["standard_metadata", "deq_qdepth"] + ], + [ + "intrinsic_metadata.ingress_global_timestamp", + ["standard_metadata", "ingress_global_timestamp"] + ], + [ + "intrinsic_metadata.egress_global_timestamp", + ["standard_metadata", "egress_global_timestamp"] + ], + [ + "intrinsic_metadata.lf_field_list", + ["standard_metadata", "lf_field_list"] + ], + [ + "intrinsic_metadata.mcast_grp", + ["standard_metadata", "mcast_grp"] + ], + [ + "intrinsic_metadata.resubmit_flag", + ["standard_metadata", "resubmit_flag"] + ], + [ + "intrinsic_metadata.egress_rid", + ["standard_metadata", "egress_rid"] + ], + [ + "intrinsic_metadata.recirculate_flag", + ["standard_metadata", "recirculate_flag"] + ], + [ + "intrinsic_metadata.priority", + ["standard_metadata", "priority"] + ] + ], + "program" : "p4src/main.p4", + "__meta__" : { + "version" : [2, 18], + "compiler" : "https://github.com/p4lang/p4c" + } +} \ No newline at end of file diff --git a/src/device/tests/p4/test-p4info.txt b/src/device/tests/p4/test-p4info.txt new file mode 100644 index 0000000000000000000000000000000000000000..6382852ad8f597786003252184153b526c66fb9e --- /dev/null +++ b/src/device/tests/p4/test-p4info.txt @@ -0,0 +1,245 @@ +pkg_info { + arch: "v1model" +} +tables { + preamble { + id: 33605373 + name: "IngressPipeImpl.l2_exact_table" + alias: "l2_exact_table" + } + match_fields { + id: 1 + name: "hdr.ethernet.dst_addr" + bitwidth: 48 + match_type: EXACT + } + action_refs { + id: 16812802 + } + action_refs { + id: 16796182 + annotations: "@defaultonly" + scope: DEFAULT_ONLY + } + const_default_action_id: 16796182 + direct_resource_ids: 318813612 + size: 1024 +} +tables { + preamble { + id: 33573501 + name: "IngressPipeImpl.l2_ternary_table" + alias: "l2_ternary_table" + } + match_fields { + id: 1 + name: "hdr.ethernet.dst_addr" + bitwidth: 48 + match_type: TERNARY + } + action_refs { + id: 16841371 + } + action_refs { + id: 16796182 + annotations: "@defaultonly" + scope: DEFAULT_ONLY + } + const_default_action_id: 16796182 + direct_resource_ids: 318768597 + size: 1024 +} +tables { + preamble { + id: 33557865 + name: "IngressPipeImpl.acl_table" + alias: "acl_table" + } + match_fields { + id: 1 + name: "standard_metadata.ingress_port" + bitwidth: 9 + match_type: TERNARY + } + match_fields { + id: 2 + name: "hdr.ethernet.dst_addr" + bitwidth: 48 + match_type: TERNARY + } + match_fields { + id: 3 + name: "hdr.ethernet.src_addr" + bitwidth: 48 + match_type: TERNARY + } + match_fields { + id: 4 + name: "hdr.ethernet.ether_type" + bitwidth: 16 + match_type: TERNARY + } + match_fields { + id: 5 + name: "local_metadata.ip_proto" + bitwidth: 8 + match_type: TERNARY + } + match_fields { + id: 6 + name: "local_metadata.icmp_type" + bitwidth: 8 + match_type: TERNARY + } + match_fields { + id: 7 + name: "local_metadata.l4_src_port" + bitwidth: 16 + match_type: TERNARY + } + match_fields { + id: 8 + name: "local_metadata.l4_dst_port" + bitwidth: 16 + match_type: TERNARY + } + action_refs { + id: 16833331 + } + action_refs { + id: 16782152 + } + action_refs { + id: 16796182 + } + action_refs { + id: 16800567 + annotations: "@defaultonly" + scope: DEFAULT_ONLY + } + direct_resource_ids: 318773822 + size: 1024 +} +actions { + preamble { + id: 16800567 + name: "NoAction" + alias: "NoAction" + } +} +actions { + preamble { + id: 16796182 + name: "IngressPipeImpl.drop" + alias: "drop" + } +} +actions { + preamble { + id: 16812802 + name: "IngressPipeImpl.set_egress_port" + alias: "set_egress_port" + } + params { + id: 1 + name: "port_num" + bitwidth: 9 + } +} +actions { + preamble { + id: 16841371 + name: "IngressPipeImpl.set_multicast_group" + alias: "set_multicast_group" + } + params { + id: 1 + name: "gid" + bitwidth: 16 + } +} +actions { + preamble { + id: 16833331 + name: "IngressPipeImpl.send_to_cpu" + alias: "send_to_cpu" + } +} +actions { + preamble { + id: 16782152 + name: "IngressPipeImpl.clone_to_cpu" + alias: "clone_to_cpu" + } +} +direct_counters { + preamble { + id: 318813612 + name: "l2_exact_table_counter" + alias: "l2_exact_table_counter" + } + spec { + unit: BOTH + } + direct_table_id: 33605373 +} +direct_counters { + preamble { + id: 318768597 + name: "l2_ternary_table_counter" + alias: "l2_ternary_table_counter" + } + spec { + unit: BOTH + } + direct_table_id: 33573501 +} +direct_counters { + preamble { + id: 318773822 + name: "acl_table_counter" + alias: "acl_table_counter" + } + spec { + unit: BOTH + } + direct_table_id: 33557865 +} +controller_packet_metadata { + preamble { + id: 67132047 + name: "packet_in" + alias: "packet_in" + annotations: "@controller_header(\"packet_in\")" + } + metadata { + id: 1 + name: "ingress_port" + bitwidth: 9 + } + metadata { + id: 2 + name: "_pad" + bitwidth: 7 + } +} +controller_packet_metadata { + preamble { + id: 67111875 + name: "packet_out" + alias: "packet_out" + annotations: "@controller_header(\"packet_out\")" + } + metadata { + id: 1 + name: "egress_port" + bitwidth: 9 + } + metadata { + id: 2 + name: "_pad" + bitwidth: 7 + } +} +type_info { +} diff --git a/src/device/tests/test_internal_p4.py b/src/device/tests/test_internal_p4.py new file mode 100644 index 0000000000000000000000000000000000000000..4907e538843dfa5d9c7833b4d02f05e483720510 --- /dev/null +++ b/src/device/tests/test_internal_p4.py @@ -0,0 +1,252 @@ +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Internal P4 driver tests. +""" + +import pytest +from device.service.drivers.p4.p4_driver import P4Driver +from device.service.drivers.p4.p4_common import ( + matches_mac, encode_mac, decode_mac, encode, + matches_ipv4, encode_ipv4, decode_ipv4, + matches_ipv6, encode_ipv6, decode_ipv6, + encode_num, decode_num +) +from .device_p4 import( + DEVICE_P4_IP_ADDR, DEVICE_P4_PORT, DEVICE_P4_DPID, DEVICE_P4_NAME, + DEVICE_P4_VENDOR, DEVICE_P4_HW_VER, DEVICE_P4_SW_VER, + DEVICE_P4_WORKERS, DEVICE_P4_GRACE_PERIOD, + DEVICE_P4_CONFIG_TABLE_ENTRY, DEVICE_P4_DECONFIG_TABLE_ENTRY) +from .mock_p4runtime_service import MockP4RuntimeService + + +@pytest.fixture(scope='session') +def p4runtime_service(): + """ + Spawn a mock P4Runtime server. + + :return: void + """ + _service = MockP4RuntimeService( + address=DEVICE_P4_IP_ADDR, port=DEVICE_P4_PORT, + max_workers=DEVICE_P4_WORKERS, + grace_period=DEVICE_P4_GRACE_PERIOD) + _service.start() + yield _service + _service.stop() + + +@pytest.fixture(scope='session') +def device_driverapi_p4(): + """ + Invoke an instance of the P4 driver. + + :return: void + """ + _driver = P4Driver( + address=DEVICE_P4_IP_ADDR, + port=DEVICE_P4_PORT, + id=DEVICE_P4_DPID, + name=DEVICE_P4_NAME, + vendor=DEVICE_P4_VENDOR, + hw_ver=DEVICE_P4_HW_VER, + sw_ver=DEVICE_P4_SW_VER) + _driver.Connect() + yield _driver + _driver.Disconnect() + + +def test_device_driverapi_p4_setconfig( + p4runtime_service: MockP4RuntimeService, + device_driverapi_p4: P4Driver): + """ + Test the SetConfig RPC of the P4 driver API. + + :param p4runtime_service: Mock P4Runtime service + :param device_driverapi_p4: instance of the P4 device driver + :return: void + """ + result = device_driverapi_p4.SetConfig( + DEVICE_P4_CONFIG_TABLE_ENTRY + ) + assert list(result) + + +def test_device_driverapi_p4_getconfig( + p4runtime_service: MockP4RuntimeService, + device_driverapi_p4: P4Driver): + """ + Test the GetConfig RPC of the P4 driver API. + + :param p4runtime_service: Mock P4Runtime service + :param device_driverapi_p4: instance of the P4 device driver + :return: void + """ + pytest.skip('Skipping test: GetConfig') + + +def test_device_driverapi_p4_getresource( + p4runtime_service: MockP4RuntimeService, + device_driverapi_p4: P4Driver): + """ + Test the GetResource RPC of the P4 driver API. + + :param p4runtime_service: Mock P4Runtime service + :param device_driverapi_p4: instance of the P4 device driver + :return: void + """ + pytest.skip('Skipping test: GetResource') + + +def test_device_driverapi_p4_deleteconfig( + p4runtime_service: MockP4RuntimeService, + device_driverapi_p4: P4Driver): + """ + Test the DeleteConfig RPC of the P4 driver API. + + :param p4runtime_service: Mock P4Runtime service + :param device_driverapi_p4: instance of the P4 device driver + :return: void + """ + result = device_driverapi_p4.DeleteConfig( + DEVICE_P4_DECONFIG_TABLE_ENTRY + ) + assert list(result) + + +def test_device_driverapi_p4_subscribe_state( + p4runtime_service: MockP4RuntimeService, + device_driverapi_p4: P4Driver): + """ + Test the SubscribeState RPC of the P4 driver API. + + :param p4runtime_service: Mock P4Runtime service + :param device_driverapi_p4: instance of the P4 device driver + :return: void + """ + pytest.skip('Skipping test: SubscribeState') + + +def test_device_driverapi_p4_getstate( + p4runtime_service: MockP4RuntimeService, + device_driverapi_p4: P4Driver): + """ + Test the GetState RPC of the P4 driver API. + + :param p4runtime_service: Mock P4Runtime service + :param device_driverapi_p4: instance of the P4 device driver + :return: void + """ + pytest.skip('Skipping test: GetState') + + +def test_device_driverapi_p4_unsubscribe_state( + p4runtime_service: MockP4RuntimeService, + device_driverapi_p4: P4Driver): + """ + Test the UnsubscribeState RPC of the P4 driver API. + + :param p4runtime_service: Mock P4Runtime service + :param device_driverapi_p4: instance of the P4 device driver + :return: void + """ + pytest.skip('Skipping test: UnsubscribeState') + + +def test_p4_common_mac(): + """ + Test MAC converters. + + :return: void + """ + wrong_mac = "aa:bb:cc:dd:ee" + assert not matches_mac(wrong_mac) + + mac = "aa:bb:cc:dd:ee:fe" + assert matches_mac(mac) + enc_mac = encode_mac(mac) + assert enc_mac == b'\xaa\xbb\xcc\xdd\xee\xfe',\ + "String-based MAC address to bytes failed" + enc_mac = encode(mac, 6*8) + assert enc_mac == b'\xaa\xbb\xcc\xdd\xee\xfe',\ + "String-based MAC address to bytes failed" + dec_mac = decode_mac(enc_mac) + assert mac == dec_mac,\ + "MAC address bytes to string failed" + + +def test_p4_common_ipv4(): + """ + Test IPv4 converters. + + :return: void + """ + assert not matches_ipv4("10.0.0.1.5") + assert not matches_ipv4("256.0.0.1") + assert not matches_ipv4("256.0.1") + assert not matches_ipv4("10001") + + ipv4 = "10.0.0.1" + assert matches_ipv4(ipv4) + enc_ipv4 = encode_ipv4(ipv4) + assert enc_ipv4 == b'\x0a\x00\x00\x01',\ + "String-based IPv4 address to bytes failed" + dec_ipv4 = decode_ipv4(enc_ipv4) + assert ipv4 == dec_ipv4,\ + "IPv4 address bytes to string failed" + + +def test_p4_common_ipv6(): + """ + Test IPv6 converters. + + :return: void + """ + assert not matches_ipv6('10.0.0.1') + assert matches_ipv6('2001:0000:85a3::8a2e:370:1111') + + ipv6 = "1:2:3:4:5:6:7:8" + assert matches_ipv6(ipv6) + enc_ipv6 = encode_ipv6(ipv6) + assert enc_ipv6 == \ + b'\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x00\x07\x00\x08',\ + "String-based IPv6 address to bytes failed" + dec_ipv6 = decode_ipv6(enc_ipv6) + assert ipv6 == dec_ipv6,\ + "IPv6 address bytes to string failed" + + +def test_p4_common_numbers(): + """ + Test numerical converters. + + :return: void + """ + num = 1337 + byte_len = 5 + enc_num = encode_num(num, byte_len * 8) + assert enc_num == b'\x00\x00\x00\x05\x39',\ + "Number to bytes conversion failed" + dec_num = decode_num(enc_num) + assert num == dec_num,\ + "Bytes to number conversion failed" + assert encode((num,), byte_len * 8) == enc_num + assert encode([num], byte_len * 8) == enc_num + + num = 256 + try: + encode_num(num, 8) + except OverflowError: + pass diff --git a/src/device/tests/test_unit_p4.py b/src/device/tests/test_unit_p4.py deleted file mode 100644 index 777ab280aa2b500c3c2b445fcecdf81024b817f3..0000000000000000000000000000000000000000 --- a/src/device/tests/test_unit_p4.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest -from device.service.drivers.p4.p4_driver import P4Driver -from .device_p4 import( - DEVICE_P4_ADDRESS, DEVICE_P4_PORT, DEVICE_P4_DPID, DEVICE_P4_NAME, - DEVICE_P4_VENDOR, DEVICE_P4_HW_VER, DEVICE_P4_SW_VER, - DEVICE_P4_PIPECONF, DEVICE_P4_WORKERS, DEVICE_P4_GRACE_PERIOD) -from .mock_p4runtime_service import MockP4RuntimeService - - -@pytest.fixture(scope='session') -def p4runtime_service(): - _service = MockP4RuntimeService( - address=DEVICE_P4_ADDRESS, port=DEVICE_P4_PORT, - max_workers=DEVICE_P4_WORKERS, - grace_period=DEVICE_P4_GRACE_PERIOD) - _service.start() - yield _service - _service.stop() - - -@pytest.fixture(scope='session') -def device_driverapi_p4(): - _driver = P4Driver( - address=DEVICE_P4_ADDRESS, - port=DEVICE_P4_PORT, - id=DEVICE_P4_DPID, - name=DEVICE_P4_NAME, - vendor=DEVICE_P4_VENDOR, - hw_ver=DEVICE_P4_HW_VER, - sw_ver=DEVICE_P4_SW_VER, - pipeconf=DEVICE_P4_PIPECONF) - _driver.Connect() - yield _driver - _driver.Disconnect() - - -def test_device_driverapi_p4_setconfig( - p4runtime_service: MockP4RuntimeService, - device_driverapi_p4: P4Driver): # pylint: disable=redefined-outer-name - device_driverapi_p4.SetConfig([]) - return - - -def test_device_driverapi_p4_getconfig( - p4runtime_service: MockP4RuntimeService, - device_driverapi_p4: P4Driver): # pylint: disable=redefined-outer-name - device_driverapi_p4.GetConfig() - return - - -def test_device_driverapi_p4_getresource( - p4runtime_service: MockP4RuntimeService, - device_driverapi_p4: P4Driver): # pylint: disable=redefined-outer-name - device_driverapi_p4.GetResource("") - return - - -def test_device_driverapi_p4_getstate( - p4runtime_service: MockP4RuntimeService, - device_driverapi_p4: P4Driver): # pylint: disable=redefined-outer-name - device_driverapi_p4.GetState() - return - - -def test_device_driverapi_p4_deleteconfig( - p4runtime_service: MockP4RuntimeService, - device_driverapi_p4: P4Driver): # pylint: disable=redefined-outer-name - device_driverapi_p4.DeleteConfig([]) - return - - -def test_device_driverapi_p4_subscribe_state( - p4runtime_service: MockP4RuntimeService, - device_driverapi_p4: P4Driver): # pylint: disable=redefined-outer-name - device_driverapi_p4.SubscribeState([]) - return - - -def test_device_driverapi_p4_unsubscribe_state( - p4runtime_service: MockP4RuntimeService, - device_driverapi_p4: P4Driver): # pylint: disable=redefined-outer-name - device_driverapi_p4.UnsubscribeState([]) - return diff --git a/src/device/tests/test_unitary_p4.py b/src/device/tests/test_unitary_p4.py index 86a669bd40deb8f7839d3e682b8a1f52f3c38e1b..43313caff33d646918b9be23c87e499185714a2c 100644 --- a/src/device/tests/test_unitary_p4.py +++ b/src/device/tests/test_unitary_p4.py @@ -12,22 +12,34 @@ # See the License for the specific language governing permissions and # limitations under the License. -import copy, grpc, logging, pytest -from common.proto.context_pb2 import Device, DeviceId +""" +P4 unit tests. +""" + +import copy +import logging +import operator +import grpc +import pytest +from common.proto.context_pb2 import ConfigActionEnum, Device, DeviceId,\ + DeviceOperationalStatusEnum from common.tools.grpc.Tools import grpc_message_to_json_string from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from device.service.DeviceService import DeviceService from device.service.driver_api._Driver import _Driver -from .PrepareTestScenario import ( # pylint: disable=unused-import +from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! - mock_service, device_service, context_client, device_client, monitoring_client, test_prepare_environment) + mock_service, device_service, context_client, device_client, + monitoring_client, test_prepare_environment) from .mock_p4runtime_service import MockP4RuntimeService try: from .device_p4 import( - DEVICE_P4, DEVICE_P4_ID, DEVICE_P4_UUID, DEVICE_P4_ADDRESS, DEVICE_P4_PORT, DEVICE_P4_WORKERS, - DEVICE_P4_GRACE_PERIOD, DEVICE_P4_CONNECT_RULES, DEVICE_P4_CONFIG_RULES) + DEVICE_P4, DEVICE_P4_ID, DEVICE_P4_UUID, + DEVICE_P4_IP_ADDR, DEVICE_P4_PORT, DEVICE_P4_WORKERS, + DEVICE_P4_GRACE_PERIOD, DEVICE_P4_CONNECT_RULES, + DEVICE_P4_CONFIG_TABLE_ENTRY, DEVICE_P4_DECONFIG_TABLE_ENTRY) ENABLE_P4 = True except ImportError: ENABLE_P4 = False @@ -35,10 +47,17 @@ except ImportError: LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) + @pytest.fixture(scope='session') def p4runtime_service(): + """ + Spawn a mock P4Runtime server. + + :return: void + """ _service = MockP4RuntimeService( - address=DEVICE_P4_ADDRESS, port=DEVICE_P4_PORT, + address=DEVICE_P4_IP_ADDR, + port=DEVICE_P4_PORT, max_workers=DEVICE_P4_WORKERS, grace_period=DEVICE_P4_GRACE_PERIOD) _service.start() @@ -47,27 +66,35 @@ def p4runtime_service(): # ----- Test Device Driver P4 -------------------------------------------------- - def test_device_p4_add_error_cases( context_client: ContextClient, # pylint: disable=redefined-outer-name device_client: DeviceClient, # pylint: disable=redefined-outer-name device_service: DeviceService): # pylint: disable=redefined-outer-name + """ + Test AddDevice RPC with wrong inputs. - if not ENABLE_P4: pytest.skip( - 'Skipping test: No P4 device has been configured') + :param context_client: context component client + :param device_client: device component client + :param device_service: device component service + :return: + """ - with pytest.raises(grpc.RpcError) as e: + if not ENABLE_P4: + pytest.skip('Skipping test: No P4 device has been configured') + + with pytest.raises(grpc.RpcError) as ex: device_p4_with_extra_rules = copy.deepcopy(DEVICE_P4) device_p4_with_extra_rules['device_config']['config_rules'].extend( DEVICE_P4_CONNECT_RULES) device_p4_with_extra_rules['device_config']['config_rules'].extend( - DEVICE_P4_CONFIG_RULES) + DEVICE_P4_CONFIG_TABLE_ENTRY) device_client.AddDevice(Device(**device_p4_with_extra_rules)) - assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT + assert ex.value.code() == grpc.StatusCode.INVALID_ARGUMENT msg_head = 'device.device_config.config_rules([' - msg_tail = ']) is invalid; RPC method AddDevice only accepts connection Config Rules that should start '\ - 'with "_connect/" tag. Others should be configured after adding the device.' - except_msg = str(e.value.details()) + msg_tail = ']) is invalid; RPC method AddDevice only accepts connection '\ + 'Config Rules that should start with "_connect/" tag. '\ + 'Others should be configured after adding the device.' + except_msg = str(ex.value.details()) assert except_msg.startswith(msg_head) and except_msg.endswith(msg_tail) @@ -76,35 +103,67 @@ def test_device_p4_add_correct( device_client: DeviceClient, # pylint: disable=redefined-outer-name device_service: DeviceService, # pylint: disable=redefined-outer-name p4runtime_service: MockP4RuntimeService): # pylint: disable=redefined-outer-name + """ + Test AddDevice RPC with correct inputs. + + :param context_client: context component client + :param device_client: device component client + :param device_service: device component service + :param p4runtime_service: Mock P4Runtime service + :return: + """ - if not ENABLE_P4: pytest.skip( - 'Skipping test: No P4 device has been configured') + if not ENABLE_P4: + pytest.skip('Skipping test: No P4 device has been configured') device_p4_with_connect_rules = copy.deepcopy(DEVICE_P4) device_p4_with_connect_rules['device_config']['config_rules'].extend( DEVICE_P4_CONNECT_RULES) device_client.AddDevice(Device(**device_p4_with_connect_rules)) driver_instance_cache = device_service.device_servicer.driver_instance_cache - driver : _Driver = driver_instance_cache.get(DEVICE_P4_UUID) + driver: _Driver = driver_instance_cache.get(DEVICE_P4_UUID) assert driver is not None + device_data = context_client.GetDevice(DeviceId(**DEVICE_P4_ID)) + config_rules = [ + ( + ConfigActionEnum.Name(config_rule.action), + config_rule.custom.resource_key, + config_rule.custom.resource_value + ) + for config_rule in device_data.device_config.config_rules + if config_rule.WhichOneof('config_rule') == 'custom' + ] + LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format( + '\n'.join(['{:s} {:s} = {:s}'.format(*config_rule) + for config_rule in config_rules]))) + def test_device_p4_get( context_client: ContextClient, # pylint: disable=redefined-outer-name device_client: DeviceClient, # pylint: disable=redefined-outer-name device_service: DeviceService, # pylint: disable=redefined-outer-name p4runtime_service: MockP4RuntimeService): # pylint: disable=redefined-outer-name + """ + Test GetDevice RPC. + + :param context_client: context component client + :param device_client: device component client + :param device_service: device component service + :param p4runtime_service: Mock P4Runtime service + :return: + """ - if not ENABLE_P4: pytest.skip( - 'Skipping test: No P4 device has been configured') + if not ENABLE_P4: + pytest.skip('Skipping test: No P4 device has been configured') initial_config = device_client.GetInitialConfig(DeviceId(**DEVICE_P4_ID)) - LOGGER.info('initial_config = {:s}'.format( - grpc_message_to_json_string(initial_config))) + assert len(initial_config.config_rules) == 0 + LOGGER.info('initial_config = %s', + grpc_message_to_json_string(initial_config)) device_data = context_client.GetDevice(DeviceId(**DEVICE_P4_ID)) - LOGGER.info('device_data = {:s}'.format( - grpc_message_to_json_string(device_data))) + LOGGER.info('device_data = %s', grpc_message_to_json_string(device_data)) def test_device_p4_configure( @@ -112,11 +171,58 @@ def test_device_p4_configure( device_client: DeviceClient, # pylint: disable=redefined-outer-name device_service: DeviceService, # pylint: disable=redefined-outer-name p4runtime_service: MockP4RuntimeService): # pylint: disable=redefined-outer-name + """ + Test ConfigureDevice RPC. - if not ENABLE_P4: pytest.skip( - 'Skipping test: No P4 device has been configured') + :param context_client: context component client + :param device_client: device component client + :param device_service: device component service + :param p4runtime_service: Mock P4Runtime service + :return: + """ - pytest.skip('Skipping test for unimplemented method') + if not ENABLE_P4: + pytest.skip('Skipping test: No P4 device has been configured') + + driver_instance_cache = device_service.device_servicer.driver_instance_cache + driver: _Driver = driver_instance_cache.get(DEVICE_P4_UUID) + assert driver is not None + + # No entries should exist at this point in time + driver_config = sorted(driver.GetConfig(), key=operator.itemgetter(0)) + assert len(driver_config) == len(driver.get_manager().get_resource_keys()) + assert driver.get_manager().count_active_entries() == 0 + + # Flip the operational status and check it is correctly flipped in Context + device_p4_with_operational_status = copy.deepcopy(DEVICE_P4) + device_p4_with_operational_status['device_operational_status'] = \ + DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED + device_client.ConfigureDevice(Device(**device_p4_with_operational_status)) + device_data = context_client.GetDevice(DeviceId(**DEVICE_P4_ID)) + assert device_data.device_operational_status == \ + DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED + + # Insert a new table entry + device_p4_with_config_rules = copy.deepcopy(DEVICE_P4) + device_p4_with_config_rules['device_config']['config_rules'].extend( + DEVICE_P4_CONFIG_TABLE_ENTRY) + device_client.ConfigureDevice(Device(**device_p4_with_config_rules)) + + device_data = context_client.GetDevice(DeviceId(**DEVICE_P4_ID)) + config_rules = [ + (ConfigActionEnum.Name(config_rule.action), + config_rule.custom.resource_key, + config_rule.custom.resource_value) + for config_rule in device_data.device_config.config_rules + if config_rule.WhichOneof('config_rule') == 'custom' + ] + LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format( + '\n'.join( + ['{:s} {:s} = {:s}'.format(*config_rule) + for config_rule in config_rules])) + ) + for config_rule in DEVICE_P4_CONFIG_TABLE_ENTRY: + assert 'custom' in config_rule def test_device_p4_deconfigure( @@ -124,11 +230,53 @@ def test_device_p4_deconfigure( device_client: DeviceClient, # pylint: disable=redefined-outer-name device_service: DeviceService, # pylint: disable=redefined-outer-name p4runtime_service: MockP4RuntimeService): # pylint: disable=redefined-outer-name + """ + Test DeconfigureDevice RPC. - if not ENABLE_P4: pytest.skip( - 'Skipping test: No P4 device has been configured') + :param context_client: context component client + :param device_client: device component client + :param device_service: device component service + :param p4runtime_service: Mock P4Runtime service + :return: + """ - pytest.skip('Skipping test for unimplemented method') + if not ENABLE_P4: + pytest.skip('Skipping test: No P4 device has been configured') + + driver_instance_cache = device_service.device_servicer.driver_instance_cache + driver: _Driver = driver_instance_cache.get(DEVICE_P4_UUID) + assert driver is not None + + # Delete a table entry + device_p4_with_config_rules = copy.deepcopy(DEVICE_P4) + device_p4_with_config_rules['device_config']['config_rules'].extend( + DEVICE_P4_DECONFIG_TABLE_ENTRY) + device_client.ConfigureDevice(Device(**device_p4_with_config_rules)) + + device_data = context_client.GetDevice(DeviceId(**DEVICE_P4_ID)) + config_rules = [ + (ConfigActionEnum.Name(config_rule.action), + config_rule.custom.resource_key, + config_rule.custom.resource_value) + for config_rule in device_data.device_config.config_rules + if config_rule.WhichOneof('config_rule') == 'custom' + ] + LOGGER.info('device_data.device_config.config_rules = \n{:s}'.format( + '\n'.join( + ['{:s} {:s} = {:s}'.format(*config_rule) + for config_rule in config_rules])) + ) + for config_rule in DEVICE_P4_CONFIG_TABLE_ENTRY: + assert 'custom' in config_rule + + # Flip the operational status and check it is correctly flipped in Context + device_p4_with_operational_status = copy.deepcopy(DEVICE_P4) + device_p4_with_operational_status['device_operational_status'] = \ + DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_DISABLED + device_client.ConfigureDevice(Device(**device_p4_with_operational_status)) + device_data = context_client.GetDevice(DeviceId(**DEVICE_P4_ID)) + assert device_data.device_operational_status == \ + DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_DISABLED def test_device_p4_delete( @@ -136,10 +284,20 @@ def test_device_p4_delete( device_client: DeviceClient, # pylint: disable=redefined-outer-name device_service: DeviceService, # pylint: disable=redefined-outer-name p4runtime_service: MockP4RuntimeService): # pylint: disable=redefined-outer-name + """ + Test DeleteDevice RPC. + + :param context_client: context component client + :param device_client: device component client + :param device_service: device component service + :param p4runtime_service: Mock P4Runtime service + :return: + """ - if not ENABLE_P4: pytest.skip('Skipping test: No P4 device has been configured') + if not ENABLE_P4: + pytest.skip('Skipping test: No P4 device has been configured') device_client.DeleteDevice(DeviceId(**DEVICE_P4_ID)) driver_instance_cache = device_service.device_servicer.driver_instance_cache - driver : _Driver = driver_instance_cache.get(DEVICE_P4_UUID) + driver: _Driver = driver_instance_cache.get(DEVICE_P4_UUID) assert driver is None