Commit 91880668 authored by Antonio Gines Buendia Lopez's avatar Antonio Gines Buendia Lopez
Browse files

Implement handling of gRPC errors and timeouts on NBI

Fixed returning generic error 500 on response when gRPC was failing. Implemented a timeout on the gRPC calls from NBI to PCE
parent 46cef737
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -40,7 +40,7 @@ spec:
            - name: ENABLE_HRAT
              value: "TRUE"
            - name: HRAT_HOST
              value: "192.168.1.168"
              value: "192.168.1.135"
            - name: HRAT_PORT
              value: "9090"
            - name: HRAT_SCHEME
+75 −0
Original line number Diff line number Diff line
@@ -13,10 +13,16 @@
# limitations under the License.
import json
import logging

import grpc
from flask_restful import Resource, request
from pathcompextended.client.PathCompExtendedClient import PathCompExtendedClient

from common.proto.context_pb2 import Empty
from nbi.service._tools.HttpStatusCodes import HTTP_BADREQUEST, HTTP_GATEWAYTIMEOUT

# Local constants (NBI shared constants cannot be modified)
HTTP_SERVICE_UNAVAILABLE = 503
from common.proto.pathcompextended_pb2 import (
    IetfNetworkSlice, LivenessProbe, NetworkContext as NetworkContextProto,
    NetworkTopology, TransportNetworkSliceL3, TransportOpticalSlice, UUID,
@@ -26,6 +32,27 @@ from common.tools.grpc.Tools import grpc_message_to_json_string

LOGGER = logging.getLogger(__name__)

# Generic gRPC error parsing: details + HTTP status. Each endpoint adds its own error message.
def _parse_grpc_error(e: grpc.RpcError):
    """Extract details and HTTP status from gRPC error. Returns (details_str, status_code)."""
    details = str(e.details()) if e.details() else str(e)
    code = e.code()
    if code == grpc.StatusCode.DEADLINE_EXCEEDED:
        return details, HTTP_GATEWAYTIMEOUT
    if code == grpc.StatusCode.UNAVAILABLE:
        return details, HTTP_SERVICE_UNAVAILABLE
    if code == grpc.StatusCode.UNREACHABLE:
        return details, HTTP_SERVICE_UNAVAILABLE
    if code in (grpc.StatusCode.FAILED_PRECONDITION, grpc.StatusCode.UNIMPLEMENTED):
        return details, HTTP_BADREQUEST
    return details, HTTP_SERVICE_UNAVAILABLE


def _grpc_error_response(e: grpc.RpcError, error_message: str):
    """Build JSON error response from gRPC error with endpoint-specific message."""
    details, status = _parse_grpc_error(e)
    return {"error": error_message, "details": details}, status


class _Resource(Resource):
    def __init__(self) -> None:
@@ -51,6 +78,9 @@ class Health(_Resource):
            LOGGER.debug(grpc_message_to_json_string(probe))
            output = json.loads(grpc_message_to_json_string(probe))
            return output
        except grpc.RpcError as e:
            LOGGER.error("Error checking health (gRPC): %s", e, exc_info=True)
            return _grpc_error_response(e, "PCE unavailable")
        except Exception as e:
            LOGGER.error("Error checking health: %s", e, exc_info=True)
            return {"error": str(e), "status": "unhealthy"}, 500
@@ -66,6 +96,9 @@ class Reset(_Resource):
            else:
                output = {"message": result.message}
            return output, 200
        except grpc.RpcError as e:
            LOGGER.error("Error resetting (gRPC): %s", e, exc_info=True)
            return _grpc_error_response(e, "PCE unavailable")
        except Exception as e:
            LOGGER.error("Error resetting: %s", e, exc_info=True)
            return {"error": str(e)}, 500
@@ -96,6 +129,9 @@ class NetworkContext(_Resource):
                "total": len(net_context.topologies),
            }
            return output
        except grpc.RpcError as e:
            LOGGER.error("Error getting network context (gRPC): %s", e, exc_info=True)
            return _grpc_error_response(e, "PCE unavailable")
        except Exception as e:
            LOGGER.error("Error getting network context: %s", e, exc_info=True)
            return {"error": str(e)}, 500
@@ -135,6 +171,9 @@ class NetworkContext(_Resource):
                output = {"message": result.message}

            return output, 201
        except grpc.RpcError as e:
            LOGGER.error("Error creating network context (gRPC): %s", e, exc_info=True)
            return _grpc_error_response(e, "PCE unavailable")
        except Exception as e:
            LOGGER.error("Error creating network context: %s", e, exc_info=True)
            return {"error": str(e)}, 500
@@ -155,6 +194,9 @@ class NetworkContextDetails(_Resource):
                "raw-json-topology": topology.raw_json_topology,
            }
            return output
        except grpc.RpcError as e:
            LOGGER.error("Error getting network context %s (gRPC): %s", id, e, exc_info=True)
            return _grpc_error_response(e, "PCE unavailable")
        except Exception as e:
            LOGGER.error("Error getting network context %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500
@@ -171,6 +213,9 @@ class NetworkContextDetails(_Resource):
            else:
                output = {"message": result.message}
            return output, 200
        except grpc.RpcError as e:
            LOGGER.error("Error deleting network context %s (gRPC): %s", id, e, exc_info=True)
            return _grpc_error_response(e, "PCE unavailable")
        except Exception as e:
            LOGGER.error("Error deleting network context %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500
@@ -211,6 +256,9 @@ class TransportOpticalSlice(_Resource):
                "data": data,
                "message": response.message,
            }
        except grpc.RpcError as e:
            LOGGER.error("Error getting transport optical slices (gRPC): %s", e, exc_info=True)
            return _grpc_error_response(e, "PCE or H-RAT unavailable")
        except Exception as e:
            LOGGER.error("Error getting transport optical slices: %s", e, exc_info=True)
            return {"error": str(e)}, 500
@@ -225,6 +273,9 @@ class TransportOpticalSlice(_Resource):
            else:
                output = {"message": result.message}
            return output, 200
        except grpc.RpcError as e:
            LOGGER.error("Error deleting all transport optical slices (gRPC): %s", e, exc_info=True)
            return _grpc_error_response(e, "PCE or H-RAT unavailable")
        except Exception as e:
            LOGGER.error("Error deleting all transport optical slices: %s", e, exc_info=True)
            return {"error": str(e)}, 500
@@ -246,6 +297,9 @@ class TransportOpticalSlice(_Resource):
            LOGGER.debug(grpc_message_to_json_string(result))
            output = _transport_optical_slice_to_hrat(result)
            return output, 201
        except grpc.RpcError as e:
            LOGGER.error("Error creating transport optical slice (gRPC): %s", e, exc_info=True)
            return _grpc_error_response(e, "PCE or H-RAT unavailable")
        except Exception as e:
            LOGGER.error("Error creating transport optical slice: %s", e, exc_info=True)
            return {"error": str(e)}, 500
@@ -262,6 +316,9 @@ class TransportOpticalSliceDetails(_Resource):
            LOGGER.debug(grpc_message_to_json_string(slice_msg))
            output = _transport_optical_slice_to_hrat(slice_msg)
            return output
        except grpc.RpcError as e:
            LOGGER.error("Error getting transport optical slice %s (gRPC): %s", id, e, exc_info=True)
            return _grpc_error_response(e, "PCE or H-RAT unavailable")
        except Exception as e:
            LOGGER.error("Error getting transport optical slice %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500
@@ -278,6 +335,9 @@ class TransportOpticalSliceDetails(_Resource):
            else:
                output = {"message": result.message}
            return output, 200
        except grpc.RpcError as e:
            LOGGER.error("Error deleting transport optical slice %s (gRPC): %s", id, e, exc_info=True)
            return _grpc_error_response(e, "PCE or H-RAT unavailable")
        except Exception as e:
            LOGGER.error("Error deleting transport optical slice %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500
@@ -299,6 +359,9 @@ class TransportNetworkSliceL3(_Resource):
                "data": data,
                "message": response.message,
            }
        except grpc.RpcError as e:
            LOGGER.error("Error getting transport network slices L3 (gRPC): %s", e, exc_info=True)
            return _grpc_error_response(e, "PCE or H-RAT unavailable")
        except Exception as e:
            LOGGER.error("Error getting transport network slices L3: %s", e, exc_info=True)
            return {"error": str(e)}, 500
@@ -313,6 +376,9 @@ class TransportNetworkSliceL3(_Resource):
            else:
                output = {"message": result.message}
            return output, 200
        except grpc.RpcError as e:
            LOGGER.error("Error deleting all transport network slices L3 (gRPC): %s", e, exc_info=True)
            return _grpc_error_response(e, "PCE or H-RAT unavailable")
        except Exception as e:
            LOGGER.error("Error deleting all transport network slices L3: %s", e, exc_info=True)
            return {"error": str(e)}, 500
@@ -334,6 +400,9 @@ class TransportNetworkSliceL3(_Resource):
            LOGGER.debug(grpc_message_to_json_string(result))
            output = _transport_network_slice_l3_to_hrat(result)
            return output, 201
        except grpc.RpcError as e:
            LOGGER.error("Error creating transport network slice L3 (gRPC): %s", e, exc_info=True)
            return _grpc_error_response(e, "PCE or H-RAT unavailable")
        except Exception as e:
            LOGGER.error("Error creating transport network slice L3: %s", e, exc_info=True)
            return {"error": str(e)}, 500
@@ -350,6 +419,9 @@ class TransportNetworkSliceL3Details(_Resource):
            LOGGER.debug(grpc_message_to_json_string(slice_msg))
            output = _transport_network_slice_l3_to_hrat(slice_msg)
            return output
        except grpc.RpcError as e:
            LOGGER.error("Error getting transport network slice L3 %s (gRPC): %s", id, e, exc_info=True)
            return _grpc_error_response(e, "PCE or H-RAT unavailable")
        except Exception as e:
            LOGGER.error("Error getting transport network slice L3 %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500
@@ -366,6 +438,9 @@ class TransportNetworkSliceL3Details(_Resource):
            else:
                output = {"message": result.message}
            return output, 200
        except grpc.RpcError as e:
            LOGGER.error("Error deleting transport network slice L3 %s (gRPC): %s", id, e, exc_info=True)
            return _grpc_error_response(e, "PCE or H-RAT unavailable")
        except Exception as e:
            LOGGER.error("Error deleting transport network slice L3 %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500
+17 −16
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ from common.tools.grpc.Tools import grpc_message_to_json_string

LOGGER = logging.getLogger(__name__)
MAX_RETRIES = 15
GRPC_TIMEOUT_SECONDS = 10
DELAY_FUNCTION = delay_exponential(initial=0.01, increment=2.0, maximum=5.0)
RETRY_DECORATOR = retry(max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect')

@@ -60,7 +61,7 @@ class PathCompExtendedClient:
        if request is None:
            request = Empty()
        LOGGER.debug('HealthCheck request')
        response = self.stub.HealthCheck(request)
        response = self.stub.HealthCheck(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('HealthCheck result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

@@ -69,7 +70,7 @@ class PathCompExtendedClient:
        if request is None:
            request = Empty()
        LOGGER.debug('Reset request')
        response = self.stub.Reset(request)
        response = self.stub.Reset(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('Reset result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

@@ -80,14 +81,14 @@ class PathCompExtendedClient:
    @RETRY_DECORATOR
    def CreateNetworkContext(self, request: NetworkContext) -> GenericMessage:
        LOGGER.debug('CreateNetworkContext request: {:s}'.format(grpc_message_to_json_string(request)))
        response = self.stub.CreateNetworkContext(request)
        response = self.stub.CreateNetworkContext(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('CreateNetworkContext result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

    @RETRY_DECORATOR
    def DeleteNetworkContext(self, request: UUID) -> GenericMessage:
        LOGGER.debug('DeleteNetworkContext request: {:s}'.format(grpc_message_to_json_string(request)))
        response = self.stub.DeleteNetworkContext(request)
        response = self.stub.DeleteNetworkContext(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('DeleteNetworkContext result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

@@ -96,14 +97,14 @@ class PathCompExtendedClient:
        if request is None:
            request = Empty()
        LOGGER.debug('GetNetworkContext request')
        response = self.stub.GetNetworkContext(request)
        response = self.stub.GetNetworkContext(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('GetNetworkContext result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

    @RETRY_DECORATOR
    def GetSpecificNetworkContext(self, request: UUID) -> NetworkTopology:
        LOGGER.debug('GetSpecificNetworkContext request: {:s}'.format(grpc_message_to_json_string(request)))
        response = self.stub.GetSpecificNetworkContext(request)
        response = self.stub.GetSpecificNetworkContext(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('GetSpecificNetworkContext result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

@@ -114,14 +115,14 @@ class PathCompExtendedClient:
    @RETRY_DECORATOR
    def CreateTransportOpticalSlice(self, request: IetfNetworkSlice) -> TransportOpticalSlice:
        LOGGER.debug('CreateTransportOpticalSlice request: {:s}'.format(grpc_message_to_json_string(request)))
        response = self.stub.CreateTransportOpticalSlice(request)
        response = self.stub.CreateTransportOpticalSlice(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('CreateTransportOpticalSlice result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

    @RETRY_DECORATOR
    def DeleteTransportOpticalSlice(self, request: UUID) -> GenericMessage:
        LOGGER.debug('DeleteTransportOpticalSlice request: {:s}'.format(grpc_message_to_json_string(request)))
        response = self.stub.DeleteTransportOpticalSlice(request)
        response = self.stub.DeleteTransportOpticalSlice(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('DeleteTransportOpticalSlice result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

@@ -130,7 +131,7 @@ class PathCompExtendedClient:
        if request is None:
            request = Empty()
        LOGGER.debug('DeleteAllTransportOpticalSlices request')
        response = self.stub.DeleteAllTransportOpticalSlices(request)
        response = self.stub.DeleteAllTransportOpticalSlices(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('DeleteAllTransportOpticalSlices result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

@@ -139,14 +140,14 @@ class PathCompExtendedClient:
        if request is None:
            request = Empty()
        LOGGER.debug('GetTransportOpticalSlices request')
        response = self.stub.GetTransportOpticalSlices(request)
        response = self.stub.GetTransportOpticalSlices(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('GetTransportOpticalSlices result: {:d} items'.format(len(response.data)))
        return response

    @RETRY_DECORATOR
    def GetTransportOpticalSlice(self, request: UUID) -> TransportOpticalSlice:
        LOGGER.debug('GetTransportOpticalSlice request: {:s}'.format(grpc_message_to_json_string(request)))
        response = self.stub.GetTransportOpticalSlice(request)
        response = self.stub.GetTransportOpticalSlice(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('GetTransportOpticalSlice result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

@@ -157,14 +158,14 @@ class PathCompExtendedClient:
    @RETRY_DECORATOR
    def CreateTransportNetworkSliceL3(self, request: IetfNetworkSlice) -> TransportNetworkSliceL3:
        LOGGER.debug('CreateTransportNetworkSliceL3 request: {:s}'.format(grpc_message_to_json_string(request)))
        response = self.stub.CreateTransportNetworkSliceL3(request)
        response = self.stub.CreateTransportNetworkSliceL3(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('CreateTransportNetworkSliceL3 result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

    @RETRY_DECORATOR
    def DeleteTransportNetworkSliceL3(self, request: UUID) -> GenericMessage:
        LOGGER.debug('DeleteTransportNetworkSliceL3 request: {:s}'.format(grpc_message_to_json_string(request)))
        response = self.stub.DeleteTransportNetworkSliceL3(request)
        response = self.stub.DeleteTransportNetworkSliceL3(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('DeleteTransportNetworkSliceL3 result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

@@ -173,7 +174,7 @@ class PathCompExtendedClient:
        if request is None:
            request = Empty()
        LOGGER.debug('DeleteAllTransportNetworkSlicesL3 request')
        response = self.stub.DeleteAllTransportNetworkSlicesL3(request)
        response = self.stub.DeleteAllTransportNetworkSlicesL3(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('DeleteAllTransportNetworkSlicesL3 result: {:s}'.format(grpc_message_to_json_string(response)))
        return response

@@ -182,14 +183,14 @@ class PathCompExtendedClient:
        if request is None:
            request = Empty()
        LOGGER.debug('GetTransportNetworkSlicesL3 request')
        response = self.stub.GetTransportNetworkSlicesL3(request)
        response = self.stub.GetTransportNetworkSlicesL3(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('GetTransportNetworkSlicesL3 result: {:d} items'.format(len(response.data)))
        return response

    @RETRY_DECORATOR
    def GetTransportNetworkSliceL3(self, request: UUID) -> TransportNetworkSliceL3:
        LOGGER.debug('GetTransportNetworkSliceL3 request: {:s}'.format(grpc_message_to_json_string(request)))
        response = self.stub.GetTransportNetworkSliceL3(request)
        response = self.stub.GetTransportNetworkSliceL3(request, timeout=GRPC_TIMEOUT_SECONDS)
        LOGGER.debug('GetTransportNetworkSliceL3 result: {:s}'.format(grpc_message_to_json_string(response)))
        return response