diff options
| author | Dimitri Staessens <dimitri@ouroboros.rocks> | 2026-06-30 18:55:59 +0200 |
|---|---|---|
| committer | Dimitri Staessens <dimitri@ouroboros.rocks> | 2026-06-30 19:04:18 +0200 |
| commit | 5f2bc27eacafb2b714317dd9cc0f286c774bdb64 (patch) | |
| tree | e9d4ae46e6ade649893e554d25d4634bda9da6f5 /ouroboros/errors.py | |
| parent | 87596aa6b7647f618ee8e7981d8e946f8edfa33e (diff) | |
| download | pyouroboros-5f2bc27eacafb2b714317dd9cc0f286c774bdb64.tar.gz pyouroboros-5f2bc27eacafb2b714317dd9cc0f286c774bdb64.zip | |
ouroboros: Derive errors from base class
Introduce ouroboros/errors.py with an OuroborosError base and
subclasses for IRM/IPCPd/flow/name/bind. Now maps C errno values onto
the appropriate Python exception.
Diffstat (limited to 'ouroboros/errors.py')
| -rw-r--r-- | ouroboros/errors.py | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/ouroboros/errors.py b/ouroboros/errors.py new file mode 100644 index 0000000..aa1e174 --- /dev/null +++ b/ouroboros/errors.py @@ -0,0 +1,246 @@ +# +# Ouroboros - Copyright (C) 2016 - 2026 +# +# Python API for Ouroboros - Exceptions and errno translation +# +# Dimitri Staessens <dimitri@ouroboros.rocks> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License +# version 2.1 as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., http://www.fsf.org/about/contact/. +# + +"""Exception hierarchy and errno translation for pyouroboros. + +The hierarchy follows stdlib precedent (``socket``, ``ssl``, ``json``): +a single root :class:`OuroborosError`, semantically-named subclasses, +and multiple inheritance from Python builtins where the semantics +match (e.g. :class:`FlowTimeout` is a :class:`TimeoutError`). +""" + +from __future__ import annotations + +import errno +import os +from importlib.metadata import PackageNotFoundError, version + +# Ouroboros-specific errno values, mirroring include/ouroboros/errno.h. +# Imported here (rather than via CFFI) so this module stays FFI-free +ENOTALLOC = 1000 # Flow is not allocated +EIPCPTYPE = 1001 # Unknown IPCP type +EIRMD = 1002 # Failed to communicate with IRMD +EIPCP = 1003 # Failed to communicate with IPCP +EIPCPSTATE = 1004 # Target in wrong state +EFLOWDOWN = 1005 # Flow is down +EFLOWPEER = 1006 # Flow is down (peer timed out) +ENAME = 1007 # Naming error +ECRYPT = 1008 # Encryption error +EAUTH = 1009 # Authentication error +EREPLAY = 1010 # OAP replay detected + + +# --- Root --- + +class OuroborosError(Exception): + """Base class for all pyouroboros exceptions.""" + + +# --- IRM-side --- + +class IrmError(OuroborosError): + """Base class for IRM (control plane) errors.""" + + +class IpcpCreateError(IrmError): + """Raised when IPCP creation fails.""" + + +class IpcpBootstrapError(IrmError): + """Raised when IPCP bootstrapping fails.""" + + +class IpcpEnrollError(IrmError): + """Raised when IPCP enrollment fails.""" + + +class IpcpConnectError(IrmError): + """Raised when IPCP connection or disconnection fails.""" + + +class IpcpTypeError(IrmError, ValueError): + """Raised when an unknown IPCP type is encountered.""" + + +class IpcpStateError(IrmError): + """Raised when an IPCP/IRMd is in the wrong state for the request.""" + + +class IrmdError(IrmError): + """Raised when the IRM daemon is unreachable.""" + + +class IpcpdError(IrmError): + """Raised when the IPCP daemon is unreachable.""" + + +class BindError(IrmError): + """Raised when binding a program/process/IPCP to a name fails.""" + + +class NameNotFoundError(IrmError): + """Raised when a name lookup or unregister target does not exist.""" + + +class NameExistsError(IrmError): + """Raised when a name already exists.""" + + +class InvalidNameError(IrmError, ValueError): + """Raised when a name is malformed or otherwise invalid.""" + + +# --- Flow / dev-side --- + +class FlowError(OuroborosError): + """Base class for flow-plane errors.""" + + +class FlowAlreadyAllocatedError(FlowError): + """Raised when allocating on a Flow that already holds an fd.""" + + +class FlowNotAllocatedError(FlowError): + """Raised when operating on a Flow that has no fd.""" + + +class FlowDownError(FlowError, ConnectionError): + """Raised when the flow has gone down.""" + + +class FlowPeerError(FlowDownError): + """Raised when the flow's peer has timed out.""" + + +class FlowPermissionError(FlowError, PermissionError): + """Raised when an operation is not permitted on the flow.""" + + +class FlowTimeout(FlowError, TimeoutError): + """Raised when a flow operation times out.""" + + +class FlowCryptError(FlowError): + """Raised on flow encryption errors.""" + + +class FlowAuthError(FlowError): + """Raised on flow authentication errors.""" + + +class FlowReplayError(FlowError): + """Raised when OAP replay is detected.""" + + +class FlowEventError(FlowError): + """Raised on errors from the flow event subsystem.""" + + +class FlowDeallocWarning(Warning): + """Warning issued when a deallocation reports a non-fatal problem.""" + + +# --- errno translation --- + +# Errno -> Python exception class. Errnos are in their canonical +# positive form here; raise_errno() flips the sign. +# +# Mapped errnos override the caller's `default=` argument, so we only +# map errnos whose semantics are unambiguous across both flow and IRM +# contexts. EPERM and EINVAL are intentionally NOT mapped: they can +# mean very different things on different operations, and a global +# mapping would silently override the call-site's chosen default (e.g. +# masking IpcpBootstrapError as a bare ValueError). +_ERRNO_MAP: dict[int, type[BaseException]] = { + errno.ETIMEDOUT: TimeoutError, + errno.EAGAIN: BlockingIOError, + errno.EWOULDBLOCK: BlockingIOError, + errno.ENOMEM: MemoryError, + errno.EACCES: PermissionError, + errno.ENOTCONN: ConnectionError, + errno.ECONNRESET: ConnectionResetError, + ENOTALLOC: FlowNotAllocatedError, + EIPCPTYPE: IpcpTypeError, + EIRMD: IrmdError, + EIPCP: IpcpdError, + EIPCPSTATE: IpcpStateError, + EFLOWDOWN: FlowDownError, + EFLOWPEER: FlowPeerError, + ENAME: NameNotFoundError, + ECRYPT: FlowCryptError, + EAUTH: FlowAuthError, + EREPLAY: FlowReplayError, +} + + +def _strerror(err: int) -> str: + if err >= 1000: + return _O7S_ERRNO_STR.get(err, f"ouroboros errno {err}") + return os.strerror(err) + + +_O7S_ERRNO_STR: dict[int, str] = { + ENOTALLOC: "flow is not allocated", + EIPCPTYPE: "unknown IPCP type", + EIRMD: "failed to communicate with IRMd", + EIPCP: "failed to communicate with IPCP", + EIPCPSTATE: "target in wrong state", + EFLOWDOWN: "flow is down", + EFLOWPEER: "flow peer timed out", + ENAME: "naming error", + ECRYPT: "encryption error", + EAUTH: "authentication error", + EREPLAY: "OAP replay detected", +} + + +def raise_errno(rc: int, default: type[OuroborosError] = FlowError) -> int: + """Translate a non-negative-or-negative-errno return code. + + Returns *rc* unchanged when it is non-negative. Otherwise raises + the mapped exception (or *default* if no mapping exists). + """ + if rc >= 0: + return rc + err = -rc + exc_cls = _ERRNO_MAP.get(err, default) + raise exc_cls(_strerror(err)) + + +def check_ouroboros_version(o7s_major: int, o7s_minor: int) -> None: + """Verify that the installed pyouroboros matches the linked library. + + Compares *o7s_major*/*o7s_minor* (from + ``OUROBOROS_VERSION_{MAJOR,MINOR}`` in the CFFI module) against + the installed pyouroboros distribution metadata. Silently returns + when running from a source tree without distribution metadata. + """ + try: + pyo7s_parts = version("PyOuroboros").split(".") + except PackageNotFoundError: + return + if o7s_major != int(pyo7s_parts[0]) or \ + o7s_minor != int(pyo7s_parts[1]): + raise RuntimeError( + f"Ouroboros version mismatch: library is " + f"{o7s_major}.{o7s_minor}, pyouroboros is " + f"{pyo7s_parts[0]}.{pyo7s_parts[1]}" + ) |
