From e3ef4d14db91e5e339f46f43fa984ed1e147206e Mon Sep 17 00:00:00 2001 From: Antonin Bas Date: Tue, 25 Sep 2018 13:39:55 -0700 Subject: [PATCH] Better output for debugging P4Runtime gRPC errors P4Runtime uses a "complex" error message format to report errors for batched Write & Read RPC requests. Some effort is required to parse the error messages appropriately in order to print some useful debugging information. --- exercises/p4runtime/mycontroller.py | 8 +-- utils/p4runtime_lib/error_utils.py | 91 +++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 utils/p4runtime_lib/error_utils.py diff --git a/exercises/p4runtime/mycontroller.py b/exercises/p4runtime/mycontroller.py index c9c766e..8c82460 100755 --- a/exercises/p4runtime/mycontroller.py +++ b/exercises/p4runtime/mycontroller.py @@ -11,6 +11,7 @@ sys.path.append( os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../utils/')) import p4runtime_lib.bmv2 +from p4runtime_lib.error_utils import printGrpcError from p4runtime_lib.switch import ShutdownAllSwitchConnections import p4runtime_lib.helper @@ -125,13 +126,6 @@ def printCounter(p4info_helper, sw, counter_name, index): counter.data.packet_count, counter.data.byte_count ) -def printGrpcError(e): - print "gRPC Error:", e.details(), - status_code = e.code() - print "(%s)" % status_code.name, - traceback = sys.exc_info()[2] - print "[%s:%d]" % (traceback.tb_frame.f_code.co_filename, traceback.tb_lineno) - def main(p4info_file_path, bmv2_file_path): # Instantiate a P4Runtime helper from the p4info file p4info_helper = p4runtime_lib.helper.P4InfoHelper(p4info_file_path) diff --git a/utils/p4runtime_lib/error_utils.py b/utils/p4runtime_lib/error_utils.py new file mode 100644 index 0000000..c3d523d --- /dev/null +++ b/utils/p4runtime_lib/error_utils.py @@ -0,0 +1,91 @@ +# Copyright 2013-present Barefoot Networks, Inc. +# +# 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 sys + +from google.rpc import status_pb2, code_pb2 +import grpc +from p4 import p4runtime_pb2 + +# Used to indicate that the gRPC error Status object returned by the server has +# an incorrect format. +class P4RuntimeErrorFormatException(Exception): + def __init__(self, message): + super(P4RuntimeErrorFormatException, self).__init__(message) + + +# Parse the binary details of the gRPC error. This is required to print some +# helpful debugging information in tha case of batched Write / Read +# requests. Returns None if there are no useful binary details and throws +# P4RuntimeErrorFormatException if the error is not formatted +# properly. Otherwise, returns a list of tuples with the first element being the +# index of the operation in the batch that failed and the second element being +# the p4.Error Protobuf message. +def parseGrpcErrorBinaryDetails(grpc_error): + if grpc_error.code() != grpc.StatusCode.UNKNOWN: + return None + + 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 grpc_error.trailing_metadata(): + if meta[0] == "grpc-status-details-bin": + error = status_pb2.Status() + error.ParseFromString(meta[1]) + break + if error is None: # no binary details field + return None + if len(error.details) == 0: + # binary details field has empty Any details repeated field + return None + + indexed_p4_errors = [] + for idx, one_error_any in enumerate(error.details): + p4_error = p4runtime_pb2.Error() + 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 + indexed_p4_errors += [(idx, p4_error)] + + return indexed_p4_errors + + +# P4Runtime uses a 3-level message in case of an error during the processing of +# a write batch. This means that some care is required when printing the +# exception if we do not want to end-up with a non-helpful message in case of +# failure as only the first level will be printed. In this function, we extract +# the nested error message when present (one for each operation included in the +# batch) in order to print error code + user-facing message. See P4Runtime +# documentation for more details on error-reporting. +def printGrpcError(grpc_error): + print "gRPC Error", grpc_error.details(), + status_code = grpc_error.code() + print "({})".format(status_code.name), + traceback = sys.exc_info()[2] + print "[{}:{}]".format( + traceback.tb_frame.f_code.co_filename, traceback.tb_lineno) + if status_code != grpc.StatusCode.UNKNOWN: + return + p4_errors = parseGrpcErrorBinaryDetails(grpc_error) + if p4_errors is None: + return + print "Errors in batch:" + for idx, p4_error in p4_errors: + code_name = code_pb2._CODE.values_by_number[ + p4_error.canonical_code].name + print "\t* At index {}: {}, '{}'\n".format( + idx, code_name, p4_error.message)