aboutsummaryrefslogtreecommitdiff
path: root/ouroboros/errors.py
diff options
context:
space:
mode:
Diffstat (limited to 'ouroboros/errors.py')
-rw-r--r--ouroboros/errors.py284
1 files changed, 284 insertions, 0 deletions
diff --git a/ouroboros/errors.py b/ouroboros/errors.py
new file mode 100644
index 0000000..6ab6053
--- /dev/null
+++ b/ouroboros/errors.py
@@ -0,0 +1,284 @@
+#
+# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+
+"""
+Exception hierarchy and errno translation for pyouroboros.
+
+The ``E*`` constants mirror the Ouroboros-specific errno values in
+``include/ouroboros/errno.h``.
+"""
+
+from __future__ import annotations
+
+import errno
+import os
+from importlib.metadata import PackageNotFoundError, version
+
+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
+
+_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",
+}
+
+
+class OuroborosError(Exception):
+ """
+ Base class for all pyouroboros exceptions.
+ """
+
+
+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 is missing.
+ """
+
+
+class NameExistsError(IrmError):
+ """
+ Raised when a name already exists.
+ """
+
+
+class InvalidNameError(IrmError, ValueError):
+ """
+ Raised when a name is malformed or otherwise invalid.
+ """
+
+
+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_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)
+
+
+def raise_errno(rc: int, default: type[OuroborosError] = FlowError) -> int:
+ """
+ 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 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(
+ f"Ouroboros version mismatch: library is "
+ f"{o7s_major}.{o7s_minor}, pyouroboros is "
+ f"{pyo7s_parts[0]}.{pyo7s_parts[1]}"
+ )