aboutsummaryrefslogtreecommitdiff
path: root/ouroboros/errors.py
diff options
context:
space:
mode:
Diffstat (limited to 'ouroboros/errors.py')
-rw-r--r--ouroboros/errors.py222
1 files changed, 130 insertions, 92 deletions
diff --git a/ouroboros/errors.py b/ouroboros/errors.py
index aa1e174..6ab6053 100644
--- a/ouroboros/errors.py
+++ b/ouroboros/errors.py
@@ -1,30 +1,13 @@
#
-# 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/.
+# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens
+# SPDX-License-Identifier: LGPL-2.1-only
#
-"""Exception hierarchy and errno translation for pyouroboros.
+"""
+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`).
+The ``E*`` constants mirror the Ouroboros-specific errno values in
+``include/ouroboros/errno.h``.
"""
from __future__ import annotations
@@ -33,8 +16,6 @@ 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
@@ -47,128 +28,177 @@ ECRYPT = 1008 # Encryption error
EAUTH = 1009 # Authentication error
EREPLAY = 1010 # OAP replay detected
+_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",
+}
-# --- Root ---
class OuroborosError(Exception):
- """Base class for all pyouroboros exceptions."""
-
+ """
+ Base class for all pyouroboros exceptions.
+ """
-# --- IRM-side ---
class IrmError(OuroborosError):
- """Base class for IRM (control plane) errors."""
+ """
+ Base class for IRM (control plane) errors.
+ """
class IpcpCreateError(IrmError):
- """Raised when IPCP creation fails."""
+ """
+ Raised when IPCP creation fails.
+ """
class IpcpBootstrapError(IrmError):
- """Raised when IPCP bootstrapping fails."""
+ """
+ Raised when IPCP bootstrapping fails.
+ """
class IpcpEnrollError(IrmError):
- """Raised when IPCP enrollment fails."""
+ """
+ Raised when IPCP enrollment fails.
+ """
class IpcpConnectError(IrmError):
- """Raised when IPCP connection or disconnection fails."""
+ """
+ Raised when IPCP connection or disconnection fails.
+ """
class IpcpTypeError(IrmError, ValueError):
- """Raised when an unknown IPCP type is encountered."""
+ """
+ Raised when an unknown IPCP type is encountered.
+ """
class IpcpStateError(IrmError):
- """Raised when an IPCP/IRMd is in the wrong state for the request."""
+ """
+ Raised when an IPCP/IRMd is in the wrong state for the request.
+ """
class IrmdError(IrmError):
- """Raised when the IRM daemon is unreachable."""
+ """
+ Raised when the IRM daemon is unreachable.
+ """
class IpcpdError(IrmError):
- """Raised when the IPCP daemon is unreachable."""
+ """
+ Raised when the IPCP daemon is unreachable.
+ """
class BindError(IrmError):
- """Raised when binding a program/process/IPCP to a name fails."""
+ """
+ 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."""
+ """
+ Raised when a name lookup or unregister target is missing.
+ """
class NameExistsError(IrmError):
- """Raised when a name already exists."""
+ """
+ Raised when a name already exists.
+ """
class InvalidNameError(IrmError, ValueError):
- """Raised when a name is malformed or otherwise invalid."""
-
+ """
+ Raised when a name is malformed or otherwise invalid.
+ """
-# --- Flow / dev-side ---
class FlowError(OuroborosError):
- """Base class for flow-plane errors."""
+ """
+ Base class for flow-plane errors.
+ """
class FlowAlreadyAllocatedError(FlowError):
- """Raised when allocating on a Flow that already holds an fd."""
+ """
+ Raised when allocating on a Flow that already holds an fd.
+ """
class FlowNotAllocatedError(FlowError):
- """Raised when operating on a Flow that has no fd."""
+ """
+ Raised when operating on a Flow that has no fd.
+ """
class FlowDownError(FlowError, ConnectionError):
- """Raised when the flow has gone down."""
+ """
+ Raised when the flow has gone down.
+ """
class FlowPeerError(FlowDownError):
- """Raised when the flow's peer has timed out."""
+ """
+ Raised when the flow's peer has timed out.
+ """
class FlowPermissionError(FlowError, PermissionError):
- """Raised when an operation is not permitted on the flow."""
+ """
+ Raised when an operation is not permitted on the flow.
+ """
class FlowTimeout(FlowError, TimeoutError):
- """Raised when a flow operation times out."""
+ """
+ Raised when a flow operation times out.
+ """
class FlowCryptError(FlowError):
- """Raised on flow encryption errors."""
+ """
+ Raised on flow encryption errors.
+ """
class FlowAuthError(FlowError):
- """Raised on flow authentication errors."""
+ """
+ Raised on flow authentication errors.
+ """
class FlowReplayError(FlowError):
- """Raised when OAP replay is detected."""
+ """
+ Raised when OAP replay is detected.
+ """
class FlowEventError(FlowError):
- """Raised on errors from the flow event subsystem."""
+ """
+ Raised on errors from the flow event subsystem.
+ """
class FlowDeallocWarning(Warning):
- """Warning issued when a deallocation reports a non-fatal problem."""
-
+ """
+ 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,
@@ -194,49 +224,57 @@ _ERRNO_MAP: dict[int, type[BaseException]] = {
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",
-}
+ return os.strerror(err)
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).
"""
+ Translate a non-negative-or-negative-errno return code.
+
+ ``_ERRNO_MAP`` holds errnos in their canonical positive form.
+
+ :param rc: The return code to translate; a negative
+ value is a negated errno.
+ :param default: Exception class to raise when the errno
+ has no mapping; a mapped errno overrides
+ it.
+ :return: *rc* unchanged when it is non-negative.
+ :raises OuroborosError: (or the builtin mapped for ``-rc``) when
+ *rc* is negative.
+ """
+
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.
"""
+ Verify that the installed pyouroboros matches the linked library.
+
+ Compares the linked library version against the installed
+ pyouroboros distribution metadata. Silently returns when running
+ from a source tree without distribution metadata.
+
+ :param o7s_major: Major version of the linked library, from
+ ``OUROBOROS_VERSION_MAJOR`` in the CFFI
+ module.
+ :param o7s_minor: Minor version of the linked library, from
+ ``OUROBOROS_VERSION_MINOR`` in the CFFI
+ module.
+ :raises RuntimeError: If the library and pyouroboros ``major.minor``
+ differ.
+ """
+
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(