aboutsummaryrefslogtreecommitdiff
path: root/ouroboros
diff options
context:
space:
mode:
Diffstat (limited to 'ouroboros')
-rw-r--r--ouroboros/__init__.py77
-rw-r--r--ouroboros/_qosspec.py61
-rw-r--r--ouroboros/_timespec.py59
-rw-r--r--ouroboros/cli.py448
-rw-r--r--ouroboros/dev.py596
-rw-r--r--ouroboros/errors.py284
-rw-r--r--ouroboros/event.py226
-rw-r--r--ouroboros/irm.py653
-rw-r--r--ouroboros/qos.py183
9 files changed, 2193 insertions, 394 deletions
diff --git a/ouroboros/__init__.py b/ouroboros/__init__.py
new file mode 100644
index 0000000..cbca95d
--- /dev/null
+++ b/ouroboros/__init__.py
@@ -0,0 +1,77 @@
+#
+# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+
+"""
+Public Python API for Ouroboros.
+
+The package surface re-exports the pure-Python helpers only: errors and
+QoS. The rest of the API is imported from its submodule: flows from
+:mod:`ouroboros.dev`, the event loop from :mod:`ouroboros.event`, the
+control plane from :mod:`ouroboros.irm` (or :mod:`ouroboros.cli`).
+"""
+
+from __future__ import annotations
+
+from importlib.metadata import PackageNotFoundError, version
+
+from ouroboros.errors import (
+ BindError,
+ FlowAuthError,
+ FlowCryptError,
+ FlowDeallocWarning,
+ FlowDownError,
+ FlowError,
+ FlowEventError,
+ FlowPeerError,
+ FlowPermissionError,
+ FlowReplayError,
+ InvalidNameError,
+ IpcpBootstrapError,
+ IpcpConnectError,
+ IpcpCreateError,
+ IpcpEnrollError,
+ IpcpStateError,
+ IpcpTypeError,
+ IpcpdError,
+ IrmError,
+ IrmdError,
+ NameExistsError,
+ NameNotFoundError,
+ OuroborosError,
+)
+from ouroboros.qos import (
+ QOS_MSG,
+ QOS_RAW,
+ QOS_RAW_SAFE,
+ QOS_RT,
+ QOS_RT_SAFE,
+ QOS_STREAM,
+ QoSService,
+ QoSSpec,
+)
+
+try:
+ __version__ = version("PyOuroboros")
+except PackageNotFoundError:
+ __version__ = "0.0.0+unknown"
+
+__all__ = [
+ "__version__",
+ # qos
+ "QoSService", "QoSSpec",
+ "QOS_MSG", "QOS_RAW", "QOS_RAW_SAFE",
+ "QOS_RT", "QOS_RT_SAFE", "QOS_STREAM",
+ # errors
+ "OuroborosError",
+ "IrmError", "IrmdError", "IpcpdError",
+ "IpcpCreateError", "IpcpBootstrapError", "IpcpEnrollError",
+ "IpcpConnectError", "IpcpTypeError", "IpcpStateError",
+ "BindError",
+ "NameNotFoundError", "NameExistsError", "InvalidNameError",
+ "FlowError", "FlowDownError", "FlowPeerError",
+ "FlowPermissionError", "FlowEventError",
+ "FlowCryptError", "FlowAuthError", "FlowReplayError",
+ "FlowDeallocWarning",
+]
diff --git a/ouroboros/_qosspec.py b/ouroboros/_qosspec.py
new file mode 100644
index 0000000..5c8b245
--- /dev/null
+++ b/ouroboros/_qosspec.py
@@ -0,0 +1,61 @@
+#
+# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+
+"""
+Shared QoSSpec <-> qosspec_t conversion helpers.
+
+CFFI types are not interchangeable across extension modules, so the
+caller passes its own ``ffi`` instance.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Optional
+
+from ouroboros.qos import QoSSpec
+
+if TYPE_CHECKING:
+ from cffi import FFI
+
+
+def qos_to_qosspec(ffi: FFI, qos: Optional[QoSSpec]) -> Any:
+ """
+ Convert a :class:`QoSSpec` to a freshly-allocated ``qosspec_t *``.
+
+ Returns ``ffi.NULL`` when *qos* is ``None``.
+ """
+
+ if qos is None:
+ return ffi.NULL
+
+ return ffi.new("qosspec_t *",
+ [qos.service,
+ qos.delay,
+ qos.bandwidth,
+ qos.availability,
+ qos.loss,
+ qos.ber,
+ qos.max_gap,
+ qos.timeout])
+
+
+def qosspec_to_qos(ffi: FFI, _qos: Any) -> Optional[QoSSpec]:
+ """
+ Convert a ``qosspec_t *`` back to a :class:`QoSSpec`.
+
+ Returns ``None`` when *_qos* is ``ffi.NULL``.
+ """
+
+ if _qos == ffi.NULL:
+ return None
+
+ return QoSSpec(service=_qos.service,
+ delay=_qos.delay,
+ bandwidth=_qos.bandwidth,
+ availability=_qos.availability,
+ loss=_qos.loss,
+ ber=_qos.ber,
+ max_gap=_qos.max_gap,
+ timeout=_qos.timeout)
diff --git a/ouroboros/_timespec.py b/ouroboros/_timespec.py
new file mode 100644
index 0000000..1656eb2
--- /dev/null
+++ b/ouroboros/_timespec.py
@@ -0,0 +1,59 @@
+#
+# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+
+"""
+Shared float-seconds <-> ``struct timespec *`` conversions.
+
+The caller passes its own ``ffi`` instance so the returned CData is
+bound to the right CFFI extension module.
+"""
+
+from __future__ import annotations
+
+from math import modf
+from typing import TYPE_CHECKING, Any, Optional
+
+if TYPE_CHECKING:
+ from cffi import FFI
+
+BILLION = 1000 * 1000 * 1000
+
+
+def fl_to_timespec(ffi: FFI, timeo: Optional[float]) -> Any:
+ """
+ Convert *timeo* (seconds, or None) to a ``struct timespec *``.
+
+ *None* yields ``ffi.NULL`` (block forever). A non-positive *timeo*
+ yields a zeroed timespec (async / poll).
+ """
+
+ if timeo is None:
+ return ffi.NULL
+
+ if timeo <= 0:
+ return ffi.new("struct timespec *", [0, 0])
+
+ frac, whole = modf(timeo)
+ _timeo = ffi.new("struct timespec *")
+ _timeo.tv_sec = int(whole)
+ _timeo.tv_nsec = int(frac * BILLION)
+
+ return _timeo
+
+
+def timespec_to_fl(ffi: FFI, _timeo: Any) -> Optional[float]:
+ """
+ Convert a ``struct timespec *`` to seconds-as-float.
+
+ ``ffi.NULL`` yields *None* (block forever).
+ """
+
+ if _timeo == ffi.NULL:
+ return None
+
+ if _timeo.tv_sec <= 0 and _timeo.tv_nsec == 0:
+ return 0.0
+
+ return _timeo.tv_sec + _timeo.tv_nsec / BILLION
diff --git a/ouroboros/cli.py b/ouroboros/cli.py
new file mode 100644
index 0000000..e887410
--- /dev/null
+++ b/ouroboros/cli.py
@@ -0,0 +1,448 @@
+#
+# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+
+"""
+Higher-level wrappers that mirror the ``irm`` CLI tool behaviour.
+
+Adds program path resolution, IPCP name-to-pid lookup and ``autobind``
+handling on top of the raw IRM API.
+"""
+
+from __future__ import annotations
+
+import shutil
+from typing import List, Optional, Set
+
+from ouroboros.errors import BindError
+from ouroboros.irm import (
+ DT_COMP,
+ MGMT_COMP,
+ DhtConfig,
+ DirConfig,
+ DtConfig,
+ EthConfig,
+ IpcpConfig,
+ IpcpInfo,
+ IpcpType,
+ LinkStateConfig,
+ NameInfo,
+ RoutingConfig,
+ Udp4Config,
+ Udp6Config,
+ UnicastConfig,
+ bind_program as _irm_bind_program,
+ bind_process as _irm_bind_process,
+ bootstrap_ipcp as _irm_bootstrap_ipcp,
+ connect_ipcp as _irm_connect_ipcp,
+ create_ipcp,
+ create_name as _irm_create_name,
+ destroy_ipcp as _irm_destroy_ipcp,
+ destroy_name,
+ disconnect_ipcp as _irm_disconnect_ipcp,
+ enroll_ipcp as _irm_enroll_ipcp,
+ list_ipcps as _irm_list_ipcps,
+ list_names as _irm_list_names,
+ reg_name as _irm_reg_name,
+ unbind_process as _irm_unbind_process,
+ unbind_program as _irm_unbind_program,
+ unreg_name as _irm_unreg_name,
+)
+from ouroboros.qos import QoSSpec
+
+__all__ = [
+ # Functions
+ "create_ipcp", "destroy_ipcp", "bootstrap_ipcp", "enroll_ipcp",
+ "connect_ipcp", "disconnect_ipcp", "list_ipcps",
+ "bind_program", "bind_process", "bind_ipcp",
+ "unbind_program", "unbind_process", "unbind_ipcp",
+ "create_name", "destroy_name", "reg_name", "unreg_name", "list_names",
+ "autoboot",
+ # Re-exported configuration types
+ "DhtConfig", "DirConfig", "DtConfig", "EthConfig",
+ "IpcpConfig", "IpcpInfo", "IpcpType",
+ "LinkStateConfig", "NameInfo", "RoutingConfig",
+ "Udp4Config", "Udp6Config", "UnicastConfig",
+]
+
+
+def _pid_of(ipcp_name: str) -> int:
+ """
+ Look up the pid of a running IPCP by its name.
+ """
+
+ for info in _irm_list_ipcps():
+ if info.name == ipcp_name:
+ return info.pid
+
+ raise ValueError(f"No IPCP named {ipcp_name!r}")
+
+
+def _collect_pids(ipcp: Optional[str],
+ ipcps: Optional[List[str]],
+ layer: Optional[str],
+ layers: Optional[List[str]]) -> Set[int]:
+ """
+ Collect the set of pids matching the given name/layer filters.
+ """
+
+ ipcp_names: List[str] = []
+
+ if ipcp is not None:
+ ipcp_names.append(ipcp)
+
+ if ipcps is not None:
+ ipcp_names.extend(ipcps)
+
+ layer_names: List[str] = []
+
+ if layer is not None:
+ layer_names.append(layer)
+
+ if layers is not None:
+ layer_names.extend(layers)
+
+ pids: Set[int] = set()
+
+ if not ipcp_names and not layer_names:
+ return pids
+
+ all_ipcps = _irm_list_ipcps()
+
+ for ipcp_name in ipcp_names:
+ for i in all_ipcps:
+ if i.name == ipcp_name:
+ pids.add(i.pid)
+ break
+
+ for lyr in layer_names:
+ for i in all_ipcps:
+ if i.layer == lyr:
+ pids.add(i.pid)
+
+ return pids
+
+
+def destroy_ipcp(name: str) -> None:
+ """
+ Destroy an IPCP by name.
+
+ :param name: Name of the IPCP to destroy.
+ :raises ValueError: If no IPCP with *name* exists.
+ """
+
+ _irm_destroy_ipcp(_pid_of(name))
+
+
+def list_ipcps(name: Optional[str] = None,
+ layer: Optional[str] = None,
+ ipcp_type: Optional[IpcpType] = None) -> List[IpcpInfo]:
+ """
+ List running IPCPs, optionally filtered.
+
+ Filters are exact matches.
+
+ :param name: Filter by IPCP name (exact match).
+ :param layer: Filter by layer name (exact match).
+ :param ipcp_type: Filter by IPCP type.
+ :return: List of matching :class:`IpcpInfo` objects.
+ """
+
+ result = _irm_list_ipcps()
+
+ if name is not None:
+ result = [i for i in result if i.name == name]
+
+ if layer is not None:
+ result = [i for i in result if i.layer == layer]
+
+ if ipcp_type is not None:
+ result = [i for i in result if i.type == ipcp_type]
+
+ return result
+
+
+def reg_name(name: str,
+ ipcp: Optional[str] = None,
+ ipcps: Optional[List[str]] = None,
+ layer: Optional[str] = None,
+ layers: Optional[List[str]] = None) -> None:
+ """
+ Register a name with IPCP(s), creating it first if needed.
+
+ Registers with every IPCP in *layer* and *layers*.
+
+ :param name: The name to register.
+ :param ipcp: Single IPCP name to register with.
+ :param ipcps: List of IPCP names to register with.
+ :param layer: Single layer name to register with.
+ :param layers: List of layer names to register with.
+ """
+
+ existing = {n.name for n in _irm_list_names()}
+ if name not in existing:
+ _irm_create_name(NameInfo(name=name))
+
+ for pid in _collect_pids(ipcp, ipcps, layer, layers):
+ _irm_reg_name(name, pid)
+
+
+def bind_program(prog: str,
+ name: str,
+ opts: int = 0,
+ argv: Optional[List[str]] = None) -> None:
+ """
+ Bind a program to a name.
+
+ :param prog: Program name or path. Bare names (without ``/``) are
+ resolved on ``PATH``.
+ :param name: Name to bind to.
+ :param opts: Bind options (e.g. ``BIND_AUTO``).
+ :param argv: Arguments passed when the program is auto-started.
+ :raises BindError: If the program is not found on ``PATH``.
+ """
+
+ if '/' not in prog:
+ resolved = shutil.which(prog)
+ if resolved is None:
+ raise BindError(f"Program {prog!r} not found on PATH")
+
+ prog = resolved
+
+ _irm_bind_program(prog, name, opts=opts, argv=argv)
+
+
+def unbind_program(prog: str, name: str) -> None:
+ """
+ Unbind a program from a name.
+
+ :param prog: Path to the program.
+ :param name: Name to unbind from.
+ """
+
+ _irm_unbind_program(prog, name)
+
+
+def bind_process(pid: int, name: str) -> None:
+ """
+ Bind a running process to a name.
+
+ :param pid: PID of the process.
+ :param name: Name to bind to.
+ """
+
+ _irm_bind_process(pid, name)
+
+
+def unbind_process(pid: int, name: str) -> None:
+ """
+ Unbind a process from a name.
+
+ :param pid: PID of the process.
+ :param name: Name to unbind from.
+ """
+
+ _irm_unbind_process(pid, name)
+
+
+def bind_ipcp(ipcp: str, name: str) -> None:
+ """
+ Bind an IPCP to a name.
+
+ :param ipcp: IPCP instance name.
+ :param name: Name to bind to.
+ :raises ValueError: If no IPCP with *ipcp* exists.
+ """
+
+ _irm_bind_process(_pid_of(ipcp), name)
+
+
+def unbind_ipcp(ipcp: str, name: str) -> None:
+ """
+ Unbind an IPCP from a name.
+
+ :param ipcp: IPCP instance name.
+ :param name: Name to unbind from.
+ :raises ValueError: If no IPCP with *ipcp* exists.
+ """
+
+ _irm_unbind_process(_pid_of(ipcp), name)
+
+
+def create_name(name: str,
+ pol_lb: Optional[int] = None,
+ info: Optional[NameInfo] = None) -> None:
+ """
+ Create a registered name.
+
+ :param name: The name to create.
+ :param pol_lb: Load-balance policy.
+ :param info: Full :class:`NameInfo`; overrides *name* and
+ *pol_lb*.
+ """
+
+ if info is not None:
+ _irm_create_name(info)
+ else:
+ ni = NameInfo(name=name)
+
+ if pol_lb is not None:
+ ni.pol_lb = pol_lb
+
+ _irm_create_name(ni)
+
+
+def list_names(name: Optional[str] = None) -> List[NameInfo]:
+ """
+ List all registered names, optionally filtered on exact name.
+
+ :param name: Filter by name (exact match).
+ :return: List of :class:`NameInfo` objects.
+ """
+
+ result = _irm_list_names()
+
+ if name is not None:
+ result = [n for n in result if n.name == name]
+
+ return result
+
+
+def unreg_name(name: str,
+ ipcp: Optional[str] = None,
+ ipcps: Optional[List[str]] = None,
+ layer: Optional[str] = None,
+ layers: Optional[List[str]] = None) -> None:
+ """
+ Unregister a name from IPCP(s).
+
+ Unregisters from every IPCP in *layer* and *layers*.
+
+ :param name: The name to unregister.
+ :param ipcp: Single IPCP name to unregister from.
+ :param ipcps: List of IPCP names to unregister from.
+ :param layer: Single layer name to unregister from.
+ :param layers: List of layer names to unregister from.
+ """
+
+ for pid in _collect_pids(ipcp, ipcps, layer, layers):
+ _irm_unreg_name(name, pid)
+
+
+def bootstrap_ipcp(name: str,
+ conf: IpcpConfig,
+ autobind: bool = False) -> None:
+ """
+ Bootstrap an IPCP, optionally binding it to its name and layer.
+
+ Autobind applies to UNICAST and BROADCAST IPCPs only, and is rolled
+ back if the bootstrap fails.
+
+ :param name: Name of the IPCP.
+ :param conf: IPCP configuration, including layer name and type.
+ :param autobind: Bind the IPCP process to its name and layer.
+ """
+
+ pid = _pid_of(name)
+ layer_name = conf.layer_name
+
+ autobind_types = (IpcpType.UNICAST, IpcpType.BROADCAST)
+ if autobind and conf.ipcp_type in autobind_types:
+ _irm_bind_process(pid, name)
+ _irm_bind_process(pid, layer_name)
+
+ try:
+ _irm_bootstrap_ipcp(pid, conf)
+ except Exception:
+ if autobind and conf.ipcp_type in autobind_types:
+ _irm_unbind_process(pid, name)
+ _irm_unbind_process(pid, layer_name)
+
+ raise
+
+
+def enroll_ipcp(name: str, dst: str,
+ autobind: bool = False) -> None:
+ """
+ Enroll an IPCP, optionally binding it to its name and layer.
+
+ :param name: Name of the IPCP.
+ :param dst: Destination name or layer to enroll with.
+ :param autobind: Bind the IPCP process to its name and to the layer
+ learned during enrollment.
+ """
+
+ pid = _pid_of(name)
+
+ _irm_enroll_ipcp(pid, dst)
+
+ if autobind:
+ for info in _irm_list_ipcps():
+ if info.pid == pid:
+ _irm_bind_process(pid, info.name)
+ _irm_bind_process(pid, info.layer)
+ break
+
+
+def connect_ipcp(name: str, dst: str, comp: str = "*",
+ qos: Optional[QoSSpec] = None) -> None:
+ """
+ Connect IPCP components to a destination.
+
+ :param name: Name of the IPCP.
+ :param dst: Destination IPCP name.
+ :param comp: ``"dt"``, ``"mgmt"``, or ``"*"`` for both.
+ :param qos: QoS specification for the dt component.
+ """
+
+ pid = _pid_of(name)
+
+ if comp in ("*", "mgmt"):
+ _irm_connect_ipcp(pid, MGMT_COMP, dst)
+
+ if comp in ("*", "dt"):
+ _irm_connect_ipcp(pid, DT_COMP, dst, qos=qos)
+
+
+def disconnect_ipcp(name: str, dst: str, comp: str = "*") -> None:
+ """
+ Disconnect IPCP components from a destination.
+
+ :param name: Name of the IPCP.
+ :param dst: Destination IPCP name.
+ :param comp: ``"dt"``, ``"mgmt"``, or ``"*"`` for both.
+ """
+
+ pid = _pid_of(name)
+
+ if comp in ("*", "mgmt"):
+ _irm_disconnect_ipcp(pid, MGMT_COMP, dst)
+
+ if comp in ("*", "dt"):
+ _irm_disconnect_ipcp(pid, DT_COMP, dst)
+
+
+def autoboot(name: str,
+ ipcp_type: IpcpType,
+ layer: str,
+ conf: Optional[IpcpConfig] = None) -> None:
+ """
+ Create, autobind and bootstrap an IPCP in one step.
+
+ :param name: Name for the IPCP.
+ :param ipcp_type: Type of IPCP to create.
+ :param layer: Layer name to bootstrap into.
+ :param conf: IPCP configuration. If *None*, a default
+ :class:`IpcpConfig` is built from *ipcp_type* and
+ *layer*.
+ """
+
+ create_ipcp(name, ipcp_type)
+
+ if conf is None:
+ conf = IpcpConfig(ipcp_type=ipcp_type, layer_name=layer)
+ else:
+ conf.layer_name = layer
+
+ bootstrap_ipcp(name, conf, autobind=True)
diff --git a/ouroboros/dev.py b/ouroboros/dev.py
index 7d29624..1397bca 100644
--- a/ouroboros/dev.py
+++ b/ouroboros/dev.py
@@ -1,398 +1,582 @@
#
-# Ouroboros - Copyright (C) 2016 - 2020
-#
-# Python API for applications
-#
-# 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
#
-from _ouroboros_cffi import ffi, lib
-import errno
+"""
+Flow allocation, acceptance and I/O for the Ouroboros dev API.
+"""
+
+from __future__ import annotations
+
+import warnings
from enum import IntFlag
-from ouroboros.qos import *
-from ouroboros.qos import _qos_to_qosspec, _fl_to_timespec, _qosspec_to_qos, _timespec_to_fl
+from typing import TYPE_CHECKING, Optional, Type
-# Some constants
-MILLION = 1000 * 1000
-BILLION = 1000 * 1000 * 1000
+from _ouroboros_dev_cffi import ffi, lib
+from ouroboros._qosspec import qos_to_qosspec, qosspec_to_qos
+from ouroboros._timespec import fl_to_timespec, timespec_to_fl
+from ouroboros.errors import (
+ FlowAlreadyAllocatedError,
+ FlowDeallocWarning,
+ FlowNotAllocatedError,
+ check_ouroboros_version,
+ raise_errno,
+)
+from ouroboros.qos import QoSSpec
-# ouroboros exceptions
-class FlowAllocatedException(Exception):
- pass
+if TYPE_CHECKING:
+ from types import TracebackType
+check_ouroboros_version(lib.OUROBOROS_VERSION_MAJOR,
+ lib.OUROBOROS_VERSION_MINOR)
-class FlowNotAllocatedException(Exception):
- pass
+class FrctFlags(IntFlag):
+ """
+ FRCT-level feature flags.
+ """
-class FlowDownException(Exception):
- pass
+ RETRANSMIT = 0o1
+ RESCNTL = 0o2
+ LINGER = 0o4
-class FlowPermissionException(Exception):
- pass
+class FlowProperties(IntFlag):
+ """
+ Flags describing flow properties and blocking behaviour.
+ """
+ READ_ONLY = 0o0
+ WRITE_ONLY = 0o1
+ READ_WRITE = 0o2
+ DOWN = 0o4
+ NON_BLOCKING_READ = 0o1000
+ NON_BLOCKING_WRITE = 0o2000
+ NON_BLOCKING = NON_BLOCKING_READ | NON_BLOCKING_WRITE
+ NO_PARTIAL_READ = 0o10000
+ NO_PARTIAL_WRITE = 0o20000
-class FlowException(Exception):
- pass
+class Flow:
+ """
+ Represents an allocated Ouroboros flow.
+ """
-class FlowDeallocWarning(Warning):
- pass
+ def __init__(self, fd: int = -1) -> None:
+ """
+ Construct a Flow wrapper.
+ :param fd: Existing flow descriptor, or -1 for an unallocated
+ flow.
+ """
-def _raise(e: int) -> None:
- if e >= 0:
- return
+ self._fd: int = fd
- print("error: " + str(e))
- if e == -errno.ETIMEDOUT:
- raise TimeoutError()
- if e == -errno.EINVAL:
- raise ValueError()
- if e == -errno.ENOMEM:
- raise MemoryError()
- else:
- raise FlowException()
+ def __enter__(self) -> Flow:
+ return self
+ def __exit__(self, exc_type: Optional[Type[BaseException]],
+ exc_value: Optional[BaseException],
+ tb: Optional[TracebackType]) -> None:
+ self.dealloc()
-class FlowProperties(IntFlag):
- ReadOnly = 0o0
- WriteOnly = 0o1
- ReadWrite = 0o2
- Down = 0o4
- NonBlockingRead = 0o1000
- NonBlockingWrite = 0o2000
- NonBlocking = NonBlockingRead | NonBlockingWrite
- NoPartialRead = 0o10000
- NoPartialWrite = 0o200000
+ def __del__(self) -> None:
+ """
+ Deallocate on collection; errors are swallowed as the library
+ may already be torn down at interpreter shutdown.
+ """
+ try:
+ self.dealloc()
+ except Exception: # pylint: disable=broad-exception-caught
+ pass
-class Flow:
+ def __repr__(self) -> str:
+ return f"Flow(fd={self._fd})"
- def __init__(self):
- self.__fd: int = -1
+ def fileno(self) -> int:
+ """
+ Return the underlying ouroboros flow descriptor.
- def __enter__(self):
- return self
+ :return: The ouroboros flow descriptor, or -1 if unallocated.
+ """
- def __exit__(self, exc_type, exc_value, tb):
- lib.flow_dealloc(self.__fd)
+ return self._fd
def alloc(self,
dst: str,
- qos: QoSSpec = None,
- timeo: float = None) -> Optional[QoSSpec]:
+ qos: Optional[QoSSpec] = None,
+ timeo: Optional[float] = None) -> Optional[QoSSpec]:
"""
- Allocates a flow with a certain QoS to a destination
+ Allocate a flow with a certain QoS to a destination.
- :param dst: The destination name (string)
- :param qos: The QoS for the requested flow (QoSSpec)
- :param timeo: The timeout for the flow allocation (None -> forever, 0->async)
- :return: The QoS for the new flow
+ :param dst: Destination name.
+ :param qos: Requested QoS.
+ :param timeo: Allocation timeout (None blocks forever, 0 is
+ async).
+ :return: The QoS the IRM granted for the new flow.
+ :raises FlowAlreadyAllocatedError: If this Flow is already
+ allocated.
+ :raises FlowError: On a negative ouroboros return code.
"""
- if self.__fd >= 0:
- raise FlowAllocatedException()
+ if self._fd >= 0:
+ raise FlowAlreadyAllocatedError()
- _qos = _qos_to_qosspec(qos)
+ _qos = qos_to_qosspec(ffi, qos)
+ _timeo = fl_to_timespec(ffi, timeo)
- _timeo = _fl_to_timespec(timeo)
+ rc = lib.flow_alloc(dst.encode(), _qos, _timeo)
- self.__fd = lib.flow_alloc(dst.encode(), _qos, _timeo)
+ raise_errno(rc)
- _raise(self.__fd)
+ self._fd = rc
- return _qosspec_to_qos(_qos)
+ return qosspec_to_qos(ffi, _qos)
def accept(self,
- timeo: float = None) -> QoSSpec:
+ timeo: Optional[float] = None) -> Optional[QoSSpec]:
"""
- Accepts new flows and returns the QoS
+ Accept an incoming flow and return its QoS.
- :param timeo: The timeout for the flow allocation (None -> forever, 0->async)
- :return: The QoS for the new flow
+ :param timeo: Accept timeout (None blocks forever, 0 is async).
+ :return: The QoS of the accepted flow.
+ :raises FlowAlreadyAllocatedError: If this Flow is already
+ allocated.
+ :raises FlowError: On a negative ouroboros return code.
"""
- if self.__fd >= 0:
- raise FlowAllocatedException()
+ if self._fd >= 0:
+ raise FlowAlreadyAllocatedError()
_qos = ffi.new("qosspec_t *")
+ _timeo = fl_to_timespec(ffi, timeo)
- _timeo = _fl_to_timespec(timeo)
+ rc = lib.flow_accept(_qos, _timeo)
- self.__fd = lib.flow_accept(_qos, _timeo)
+ raise_errno(rc)
- _raise(self.__fd)
+ self._fd = rc
- return _qosspec_to_qos(_qos)
+ return qosspec_to_qos(ffi, _qos)
def join(self,
dst: str,
- qos: QoSSpec = None,
- timeo: float = None) -> Optional[QoSSpec]:
+ timeo: Optional[float] = None) -> None:
"""
- Join a broadcast layer
+ Join a broadcast layer.
- :param dst: The destination broadcast layer name (string)
- :param qos: The QoS for the requested flow (QoSSpec)
- :param timeo: The timeout for the flow allocation (None -> forever, 0->async)
- :return: The QoS for the flow
+ :param dst: Broadcast layer name.
+ :param timeo: Join timeout (None blocks forever, 0 is async).
+ :raises FlowAlreadyAllocatedError: If this Flow is already
+ allocated.
+ :raises FlowError: On a negative ouroboros return code.
"""
- if self.__fd >= 0:
- raise FlowAllocatedException()
-
- _qos = _qos_to_qosspec(qos)
+ if self._fd >= 0:
+ raise FlowAlreadyAllocatedError()
- _timeo = _fl_to_timespec(timeo)
+ _timeo = fl_to_timespec(ffi, timeo)
- self.__fd = lib.flow_join(dst.encode(), _qos, _timeo)
+ rc = lib.flow_join(dst.encode(), _timeo)
- _raise(self.__fd)
+ raise_errno(rc)
- return _qosspec_to_qos(_qos)
+ self._fd = rc
- def dealloc(self):
+ def dealloc(self) -> None:
"""
- Deallocate a flow
-
+ Deallocate this flow; a no-op on an unallocated flow.
"""
- self.__fd = lib.flow_dealloc(self.__fd)
+ if self._fd < 0:
+ return
- if self.__fd < 0:
- raise FlowDeallocWarning
+ rc = lib.flow_dealloc(self._fd)
+ self._fd = -1
- self.__fd = -1
+ if rc < 0:
+ warnings.warn(f"flow_dealloc returned {rc}",
+ FlowDeallocWarning, stacklevel=2)
def write(self,
buf: bytes,
- count: int = None) -> int:
+ count: Optional[int] = None) -> int:
"""
- Attempt to write <count> bytes to a flow
+ Write up to *count* bytes to the flow.
- :param buf: Buffer to write from
- :param count: Number of bytes to write from the buffer
- :return: Number of bytes written
+ :param buf: Buffer to write from.
+ :param count: Number of bytes to write (defaults to
+ ``len(buf)``).
+ :return: Number of bytes written.
+ :raises FlowError: On a negative ouroboros return code.
+ :raises FlowNotAllocatedError: If the flow is not allocated.
"""
- if self.__fd < 0:
- raise FlowNotAllocatedException()
+ if self._fd < 0:
+ raise FlowNotAllocatedError()
if count is None:
- return lib.flow_write(self.__fd, ffi.from_buffer(buf), len(buf))
- else:
- return lib.flow_write(self.__fd, ffi.from_buffer(buf), count)
+ count = len(buf)
+
+ rc = lib.flow_write(self._fd, ffi.from_buffer(buf), count)
+
+ return raise_errno(rc)
def writeline(self,
ln: str) -> int:
"""
- Attempt to write a string to a flow
+ Encode *ln* as UTF-8 and write it to the flow.
- :param ln: String to write
- :return: Number of bytes written
+ :param ln: String to write.
+ :return: Number of bytes written.
+ :raises FlowNotAllocatedError: If the flow is not allocated.
"""
- if self.__fd < 0:
- raise FlowNotAllocatedException()
+ if self._fd < 0:
+ raise FlowNotAllocatedError()
- return self.write(ln.encode(), len(ln))
+ data = ln.encode()
+
+ return self.write(data, len(data))
def read(self,
- count: int = None) -> bytes:
+ count: Optional[int] = None) -> bytes:
"""
- Attempt to read bytes from a flow
+ Read up to *count* bytes from the flow.
- :param count: Maximum number of bytes to read
- :return: Bytes read
+ :param count: Maximum number of bytes to read (default 2048).
+ :return: Bytes read; empty when a previous read fully
+ consumed the SDU.
+ :raises FlowError: On a negative ouroboros return code.
+ :raises FlowNotAllocatedError: If the flow is not allocated.
"""
- if self.__fd < 0:
- raise FlowNotAllocatedException()
+ if self._fd < 0:
+ raise FlowNotAllocatedError()
if count is None:
count = 2048
_buf = ffi.new("char []", count)
- result = lib.flow_read(self.__fd, _buf, count)
+ rc = lib.flow_read(self._fd, _buf, count)
+
+ raise_errno(rc)
- return ffi.unpack(_buf, result)
+ return ffi.unpack(_buf, rc)
- def readline(self):
+ def readline(self) -> str:
"""
+ Read from the flow and decode the result as UTF-8.
- :return: A string
+ :return: Decoded UTF-8 string read from the flow.
+ :raises FlowNotAllocatedError: If the flow is not allocated.
"""
- if self.__fd < 0:
- raise FlowNotAllocatedException()
+
+ if self._fd < 0:
+ raise FlowNotAllocatedError()
return self.read().decode()
- # flow manipulation
- def set_snd_timeout(self,
- timeo: float):
+ def set_snd_timeout(self, timeo: float) -> None:
"""
- Set the timeout for blocking writes
+ Set the timeout for blocking writes (seconds).
+
+ :param timeo: Write timeout in seconds.
+ :raises FlowError: On a negative ouroboros return code.
"""
- _timeo = _fl_to_timespec(timeo)
- if lib.flow_set_snd_timout(self.__fd, _timeo) != 0:
- raise FlowPermissionException()
+ _timeo = fl_to_timespec(ffi, timeo)
- def get_snd_timeout(self) -> float:
+ raise_errno(lib.flow_set_snd_timeout(self._fd, _timeo))
+
+ def get_snd_timeout(self) -> Optional[float]:
"""
- Get the timeout for blocking writes
+ Return the timeout for blocking writes (seconds).
- :return: timeout for blocking writes
+ :return: Timeout for blocking writes, in seconds.
+ :raises FlowError: On a negative ouroboros return code.
"""
+
_timeo = ffi.new("struct timespec *")
- if lib.flow_get_snd_timeout(self.__fd, _timeo) != 0:
- raise FlowPermissionException()
+ raise_errno(lib.flow_get_snd_timeout(self._fd, _timeo))
- return _timespec_to_fl(_timeo)
+ return timespec_to_fl(ffi, _timeo)
- def set_rcv_timeout(self,
- timeo: float):
+ def set_rcv_timeout(self, timeo: float) -> None:
"""
- Set the timeout for blocking writes
+ Set the timeout for blocking reads (seconds).
+
+ :param timeo: Read timeout in seconds.
+ :raises FlowError: On a negative ouroboros return code.
"""
- _timeo = _fl_to_timespec(timeo)
- if lib.flow_set_rcv_timout(self.__fd, _timeo) != 0:
- raise FlowPermissionException()
+ _timeo = fl_to_timespec(ffi, timeo)
- def get_rcv_timeout(self) -> float:
+ raise_errno(lib.flow_set_rcv_timeout(self._fd, _timeo))
+
+ def get_rcv_timeout(self) -> Optional[float]:
"""
- Get the timeout for blocking reads
+ Return the timeout for blocking reads (seconds).
- :return: timeout for blocking writes
+ :return: Timeout for blocking reads, in seconds.
+ :raises FlowError: On a negative ouroboros return code.
"""
+
_timeo = ffi.new("struct timespec *")
- if lib.flow_get_rcv_timeout(self.__fd, _timeo) != 0:
- raise FlowPermissionException()
+ raise_errno(lib.flow_get_rcv_timeout(self._fd, _timeo))
- return _timespec_to_fl(_timeo)
+ return timespec_to_fl(ffi, _timeo)
- def get_qos(self) -> QoSSpec:
+ def get_qos(self) -> Optional[QoSSpec]:
"""
+ Return the current QoS in effect on the flow.
- :return: Current QoS on the flow
+ :return: The QoS currently in effect on the flow.
+ :raises FlowError: On a negative ouroboros return code.
"""
+
_qos = ffi.new("qosspec_t *")
- if lib.flow_get_qos(self.__fd, _qos) != 0:
- raise FlowPermissionException()
+ raise_errno(lib.flow_get_qos(self._fd, _qos))
- return _qosspec_to_qos(_qos)
+ return qosspec_to_qos(ffi, _qos)
def get_rx_queue_len(self) -> int:
"""
+ Return the receive queue length (bytes).
- :return:
+ :return: Receive queue length in bytes.
+ :raises FlowError: On a negative ouroboros return code.
"""
size = ffi.new("size_t *")
- if lib.flow_get_rx_qlen(self.__fd, size) != 0:
- raise FlowPermissionException()
+ raise_errno(lib.flow_get_rx_qlen(self._fd, size))
- return int(size)
+ return int(size[0])
def get_tx_queue_len(self) -> int:
"""
+ Return the transmit queue length (bytes).
- :return:
+ :return: Transmit queue length in bytes.
+ :raises FlowError: On a negative ouroboros return code.
"""
size = ffi.new("size_t *")
- if lib.flow_get_tx_qlen(self.__fd, size) != 0:
- raise FlowPermissionException()
+ raise_errno(lib.flow_get_tx_qlen(self._fd, size))
+
+ return int(size[0])
+
+ def get_mtu(self) -> int:
+ """
+ Return the maximum user payload that fits in one n-1 PDU after
+ crypto headers (bytes), or 0 if unknown.
+
+ :return: Maximum user payload in bytes, or 0 if unknown.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
+ mtu = ffi.new("size_t *")
+
+ raise_errno(lib.flow_get_mtu(self._fd, mtu))
+
+ return int(mtu[0])
+
+ def set_flags(self, flags: FlowProperties) -> None:
+ """
+ Replace the full set of flags for this flow.
+
+ :param flags: Bitmask of :class:`FlowProperties` values.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
+ raise_errno(lib.flow_set_flags(self._fd, int(flags)))
+
+ def add_flags(self, flags: FlowProperties) -> None:
+ """
+ OR *flags* into the current flags, leaving other bits set.
+
+ :param flags: :class:`FlowProperties` bits to set.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
+ current = self.get_flags()
- return int(size)
+ self.set_flags(current | FlowProperties(int(flags)))
- def set_flags(self, flags: FlowProperties):
+ def remove_flags(self, flags: FlowProperties) -> None:
"""
- Set flags for this flow.
- :param flags:
+ Clear *flags* from the current flags, leaving other bits set.
+
+ :param flags: :class:`FlowProperties` bits to clear.
+ :raises FlowError: On a negative ouroboros return code.
"""
- _flags = ffi.new("uint32_t *", int(flags))
+ current = self.get_flags()
- if lib.flow_set_flag(self.__fd, _flags):
- raise FlowPermissionException()
+ self.set_flags(current & ~FlowProperties(int(flags)))
def get_flags(self) -> FlowProperties:
"""
- Get the flags for this flow
+ Return the current flags for this flow.
+
+ :return: The current :class:`FlowProperties` bitmask.
+ :raises FlowError: On a negative ouroboros return code.
"""
- flags = lib.flow_get_flag(self.__fd)
- if flags < 0:
- raise FlowPermissionException()
+ flags = raise_errno(lib.flow_get_flags(self._fd))
return FlowProperties(int(flags))
+ def set_frct_flags(self, flags: FrctFlags) -> None:
+ """
+ Set FRCT flags for this flow.
+
+ :param flags: Bitmask of :class:`FrctFlags`.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
+ raise_errno(lib.flow_set_frct_flags(self._fd, int(flags)))
+
+ def get_frct_flags(self) -> FrctFlags:
+ """
+ Return the FRCT flags for this flow.
+
+ :return: The current :class:`FrctFlags` bitmask.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
+ flags = raise_errno(lib.flow_get_frct_flags(self._fd))
+
+ return FrctFlags(int(flags))
+
+ def set_frct_max_sdu(self, size: int) -> None:
+ """
+ Set the maximum reassembly SDU size for FRCT (bytes).
+
+ :param size: Maximum reassembly SDU size in bytes.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
+ raise_errno(lib.flow_set_frct_max_sdu(self._fd, size))
+
+ def get_frct_max_sdu(self) -> int:
+ """
+ Return the maximum reassembly SDU size for FRCT (bytes).
+
+ :return: Maximum reassembly SDU size in bytes.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
+ size = ffi.new("size_t *")
+
+ raise_errno(lib.flow_get_frct_max_sdu(self._fd, size))
+
+ return int(size[0])
+
+ def set_frct_rcv_ring_size(self, size: int) -> None:
+ """
+ Set the stream receive ring size (bytes, power of two).
+
+ :param size: Stream receive ring size in bytes (power of two).
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
+ raise_errno(lib.flow_set_frct_rcv_ring_sz(self._fd, size))
+
+ def get_frct_rcv_ring_size(self) -> int:
+ """
+ Return the stream receive ring size (bytes).
+
+ :return: Stream receive ring size in bytes.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
+ size = ffi.new("size_t *")
+
+ raise_errno(lib.flow_get_frct_rcv_ring_sz(self._fd, size))
+
+ return int(size[0])
+
+ def set_frct_rto_min(self, rto_ns: int) -> None:
+ """
+ Set the FRCT RTO floor (nanoseconds).
+
+ :param rto_ns: FRCT RTO floor in nanoseconds.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
+ raise_errno(lib.flow_set_frct_rto_min(self._fd, rto_ns))
+
+ def get_frct_rto_min(self) -> int:
+ """
+ Return the FRCT RTO floor (nanoseconds).
+
+ :return: FRCT RTO floor in nanoseconds.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
+ rto = ffi.new("time_t *")
+
+ raise_errno(lib.flow_get_frct_rto_min(self._fd, rto))
+
+ return int(rto[0])
+
def flow_alloc(dst: str,
- qos: QoSSpec = None,
- timeo: float = None) -> Flow:
+ qos: Optional[QoSSpec] = None,
+ timeo: Optional[float] = None) -> Flow:
"""
+ Allocate a new flow and return the :class:`Flow` wrapper.
- :param dst: Destination name
- :param qos: Requested QoS
- :param timeo: Timeout to wait for the allocation
- :return: A new Flow()
+ :param dst: Destination name.
+ :param qos: Requested QoS.
+ :param timeo: Allocation timeout (None blocks forever, 0 is async).
+ :return: The allocated flow.
"""
f = Flow()
+
f.alloc(dst, qos, timeo)
+
return f
-def flow_accept(timeo: float = None) -> Flow:
+def flow_accept(timeo: Optional[float] = None) -> Flow:
"""
+ Accept an incoming flow and return the :class:`Flow` wrapper.
- :param timeo: Timeout to wait for the allocation
- :return: A new Flow()
+ :param timeo: Accept timeout (None blocks forever, 0 is async).
+ :return: The accepted flow.
"""
f = Flow()
+
f.accept(timeo)
+
return f
def flow_join(dst: str,
- qos: QoSSpec = None,
- timeo: float = None) -> Flow:
+ timeo: Optional[float] = None) -> Flow:
"""
+ Join a broadcast layer and return the :class:`Flow` wrapper.
- :param dst: Broadcast layer name
- :param qos: Requested QoS
- :param timeo: Timeout to wait for the allocation
- :return: A new Flow()
+ :param dst: Broadcast layer name.
+ :param timeo: Join timeout (None blocks forever, 0 is async).
+ :return: The joined broadcast flow.
"""
f = Flow()
- f.join(dst, qos, timeo)
- return f
+ f.join(dst, timeo)
+ return f
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]}"
+ )
diff --git a/ouroboros/event.py b/ouroboros/event.py
index b707c1b..24b58b8 100644
--- a/ouroboros/event.py
+++ b/ouroboros/event.py
@@ -1,147 +1,197 @@
#
-# Ouroboros - Copyright (C) 2016 - 2020
-#
-# Python API for applications
-#
-# 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
#
-from ouroboros.dev import *
-from ouroboros.qos import _fl_to_timespec
+"""
+Asynchronous flow event monitoring for Ouroboros.
+"""
+from __future__ import annotations
-# async API
-class FlowEventError(Exception):
- pass
+from enum import IntFlag
+from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Type
+
+from _ouroboros_dev_cffi import ffi, lib
+
+from ouroboros._timespec import fl_to_timespec
+from ouroboros.dev import Flow
+from ouroboros.errors import FlowEventError, raise_errno
+
+if TYPE_CHECKING:
+ from types import TracebackType
class FEventType(IntFlag):
- FlowPkt = lib.FLOW_PKT
- FlowDown = lib.FLOW_DOWN
- FlowUp = lib.FLOW_UP
- FlowAlloc = lib.FLOW_ALLOC
- FlowDealloc = lib.FLOW_DEALLOC
+ """
+ Types of flow events.
+ """
+
+ FLOW_PKT = lib.FLOW_PKT
+ FLOW_DOWN = lib.FLOW_DOWN
+ FLOW_UP = lib.FLOW_UP
+ FLOW_ALLOC = lib.FLOW_ALLOC
+ FLOW_DEALLOC = lib.FLOW_DEALLOC
+ FLOW_PEER = lib.FLOW_PEER
class FEventQueue:
"""
- A queue of events waiting to be handled
+ A queue of flow events waiting to be drained.
"""
- def __init__(self):
- self.__fq = lib.fqueue_create()
- if self.__fq is ffi.NULL:
+ def __init__(self) -> None:
+ self._fq = lib.fqueue_create()
+ if self._fq == ffi.NULL:
raise MemoryError("Failed to create FEventQueue")
- def __del__(self):
- lib.fqueue_destroy(self.__fq)
+ def __enter__(self) -> FEventQueue:
+ return self
+
+ def __exit__(self, exc_type: Optional[Type[BaseException]],
+ exc_value: Optional[BaseException],
+ tb: Optional[TracebackType]) -> None:
+ self.destroy()
- def next(self):
+ def __del__(self) -> None:
+ self.destroy()
+
+ def destroy(self) -> None:
"""
- Get the next event
- :return: Flow and eventtype on that flow
+ Destroy the underlying queue. Idempotent.
"""
- f = Flow()
- f._Flow__fd = lib.fqueue_next(self.__fq)
- if f._Flow__fd < 0:
- raise FlowEventError
- _type = lib.fqueue_type(self.__fq)
- if _type < 0:
- raise FlowEventError
+ if self._fq != ffi.NULL:
+ lib.fqueue_destroy(self._fq)
+
+ self._fq = ffi.NULL
+
+ def next(self) -> Tuple[Flow, FEventType]:
+ """
+ Return the next ``(flow, event_type)`` pair from the queue.
+
+ :return: The :class:`~ouroboros.dev.Flow` the event occurred
+ on, and its :class:`FEventType`.
+ :raises FlowEventError: On a negative ouroboros return code.
+ """
- return f, _type
+ fd = lib.fqueue_next(self._fq)
+
+ raise_errno(fd, default=FlowEventError)
+
+ _type = lib.fqueue_type(self._fq)
+
+ raise_errno(_type, default=FlowEventError)
+
+ return Flow(fd), FEventType(_type)
+
+ @property
+ def handle(self) -> Any:
+ """
+ Return the underlying ``fqueue_t *``.
+
+ :return: The underlying ``fqueue_t *`` CFFI pointer.
+ """
+
+ return self._fq
class FlowSet:
"""
- A set of flows that can be monitored for events
+ A set of flows that can be monitored for events.
"""
- def __init__(self,
- flows: [Flow] = None):
- self.__set = lib.fset_create()
- if self.__set is ffi.NULL:
+ def __init__(self, flows: Optional[List[Flow]] = None) -> None:
+ self._set = lib.fset_create()
+ if self._set == ffi.NULL:
raise MemoryError("Failed to create FlowSet")
if flows is not None:
for flow in flows:
- if lib.fset_add(self.__set, flow._Flow__fd) != 0:
- lib.fset_destroy(self.__set)
- self.__set = ffi.NULL
- raise MemoryError("Failed to add flow " + str(flow._Flow__fd) + ".")
+ if lib.fset_add(self._set, flow.fileno()) != 0:
+ lib.fset_destroy(self._set)
- def __enter__(self):
+ self._set = ffi.NULL
+ raise MemoryError(f"Failed to add flow {flow.fileno()}.")
+
+ def __enter__(self) -> FlowSet:
return self
- def __exit__(self, exc_type, exc_value, tb):
- lib.fset_destroy(self.__set)
+ def __exit__(self, exc_type: Optional[Type[BaseException]],
+ exc_value: Optional[BaseException],
+ tb: Optional[TracebackType]) -> None:
+ self.destroy()
+
+ def __del__(self) -> None:
+ self.destroy()
- def add(self,
- flow: Flow):
+ def destroy(self) -> None:
+ """
+ Destroy the underlying set. Idempotent.
"""
- Add a Flow
- :param flow: The flow object to add
+ if self._set != ffi.NULL:
+ lib.fset_destroy(self._set)
+
+ self._set = ffi.NULL
+
+ def add(self, flow: Flow) -> None:
"""
+ Add *flow* to this set.
- if self.__set is ffi.NULL:
- raise ValueError
+ :param flow: The flow to add to this set.
+ :raises ValueError: If the FlowSet has been destroyed.
+ :raises MemoryError: If the flow could not be added.
+ """
+
+ if self._set == ffi.NULL:
+ raise ValueError("FlowSet has been destroyed")
- if lib.fset_add(self.__set, flow._Flow___fd) != 0:
- raise MemoryError("Failed to add flow")
+ if lib.fset_add(self._set, flow.fileno()) != 0:
+ raise MemoryError(f"Failed to add flow {flow.fileno()}")
- def zero(self):
+ def zero(self) -> None:
"""
- Remove all Flows from this set
+ Remove all flows from this set.
+
+ :raises ValueError: If the FlowSet has been destroyed.
"""
- if self.__set is ffi.NULL:
- raise ValueError
+ if self._set == ffi.NULL:
+ raise ValueError("FlowSet has been destroyed")
- lib.fset_zero(self.__set)
+ lib.fset_zero(self._set)
- def remove(self,
- flow: Flow):
+ def remove(self, flow: Flow) -> None:
"""
- Remove a flow from a set
+ Remove *flow* from this set.
- :param flow:
+ :param flow: The flow to remove from this set.
+ :raises ValueError: If the FlowSet has been destroyed.
"""
- if self.__set is ffi.NULL:
- raise ValueError
+ if self._set == ffi.NULL:
+ raise ValueError("FlowSet has been destroyed")
- lib.fset_del(self.__set, flow._Flow__fd)
+ lib.fset_del(self._set, flow.fileno())
def wait(self,
- fq: FEventType,
- timeo: float = None):
+ fq: FEventQueue,
+ timeo: Optional[float] = None) -> None:
"""
- Wait for at least one event on one of the monitored flows
+ Wait for at least one event on one of the monitored flows.
+
+ :param fq: The :class:`FEventQueue` to drain events into.
+ :param timeo: Wait timeout in seconds (None blocks forever).
+ :raises ValueError: If the FlowSet has been destroyed.
+ :raises FlowEventError: On a negative ouroboros return code.
"""
- if self.__set is ffi.NULL:
- raise ValueError
+ if self._set == ffi.NULL:
+ raise ValueError("FlowSet has been destroyed")
- _timeo = _fl_to_timespec(timeo)
+ _timeo = fl_to_timespec(ffi, timeo)
- ret = lib.fevent(self.__set, fq._FEventQueue__fq, _timeo)
- if ret < 0:
- raise FlowEventError
+ rc = lib.fevent(self._set, fq.handle, _timeo)
- def destroy(self):
- lib.fset_destroy(self.__set)
+ raise_errno(rc, default=FlowEventError)
diff --git a/ouroboros/irm.py b/ouroboros/irm.py
new file mode 100644
index 0000000..ea7d665
--- /dev/null
+++ b/ouroboros/irm.py
@@ -0,0 +1,653 @@
+#
+# SPDX-FileCopyrightText: 2016 - 2026 Dimitri Staessens
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+
+"""
+IRM (IPC Resource Manager) bindings for Ouroboros.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from enum import IntEnum
+from typing import Any, List, Optional
+
+from _ouroboros_irm_cffi import ffi, lib
+
+from ouroboros._qosspec import qos_to_qosspec
+from ouroboros.errors import (
+ BindError,
+ IpcpBootstrapError,
+ IpcpConnectError,
+ IpcpCreateError,
+ IpcpEnrollError,
+ IrmError,
+ NameExistsError,
+ NameNotFoundError,
+ check_ouroboros_version,
+ raise_errno,
+)
+from ouroboros.qos import QoSSpec
+
+check_ouroboros_version(lib.OUROBOROS_VERSION_MAJOR,
+ lib.OUROBOROS_VERSION_MINOR)
+
+BIND_AUTO = lib.BIND_AUTO
+
+DT_COMP = "Data Transfer"
+MGMT_COMP = "Management"
+
+
+class IpcpType(IntEnum):
+ """
+ IPCP types available in Ouroboros.
+ """
+
+ LOCAL = lib.IPCP_LOCAL
+ UNICAST = lib.IPCP_UNICAST
+ BROADCAST = lib.IPCP_BROADCAST
+ ETH_LLC = lib.IPCP_ETH_LLC
+ ETH_DIX = lib.IPCP_ETH_DIX
+ UDP4 = lib.IPCP_UDP4
+ UDP6 = lib.IPCP_UDP6
+
+
+class AddressAuthPolicy(IntEnum):
+ """
+ Address authority policies for unicast IPCPs.
+ """
+
+ FLAT_RANDOM = lib.ADDR_AUTH_FLAT_RANDOM
+
+
+class LinkStatePolicy(IntEnum):
+ """
+ Link state routing policies.
+ """
+
+ SIMPLE = lib.LS_SIMPLE
+ LFA = lib.LS_LFA
+ ECMP = lib.LS_ECMP
+
+
+class RoutingPolicy(IntEnum):
+ """
+ Routing policies.
+ """
+
+ LINK_STATE = lib.ROUTING_LINK_STATE
+
+
+class CongestionAvoidPolicy(IntEnum):
+ """
+ Congestion avoidance policies.
+ """
+
+ NONE = lib.CA_NONE
+ MB_ECN = lib.CA_MB_ECN
+
+
+class DirectoryPolicy(IntEnum):
+ """
+ Directory policies.
+ """
+
+ DHT = lib.DIR_DHT
+
+
+class DirectoryHashAlgo(IntEnum):
+ """
+ Directory hash algorithms.
+ """
+
+ SHA3_224 = lib.DIR_HASH_SHA3_224
+ SHA3_256 = lib.DIR_HASH_SHA3_256
+ SHA3_384 = lib.DIR_HASH_SHA3_384
+ SHA3_512 = lib.DIR_HASH_SHA3_512
+
+
+class LoadBalancePolicy(IntEnum):
+ """
+ Load balancing policies for names.
+ """
+
+ ROUND_ROBIN = lib.LB_RR
+ SPILL = lib.LB_SPILL
+
+
+@dataclass
+class LinkStateConfig:
+ """
+ Configuration for link state routing.
+ """
+
+ pol: LinkStatePolicy = LinkStatePolicy.SIMPLE
+ t_recalc: int = 4
+ t_update: int = 15
+ t_timeo: int = 60
+
+
+@dataclass
+class RoutingConfig:
+ """
+ Routing configuration.
+ """
+
+ pol: RoutingPolicy = RoutingPolicy.LINK_STATE
+ ls: LinkStateConfig = field(default_factory=LinkStateConfig)
+
+
+@dataclass
+class DtConfig:
+ """
+ Data transfer configuration for unicast IPCPs.
+ """
+
+ addr_size: int = 4
+ eid_size: int = 8
+ max_ttl: int = 60
+ routing: RoutingConfig = field(default_factory=RoutingConfig)
+
+
+@dataclass
+class DhtConfig:
+ """
+ DHT directory configuration.
+ """
+
+ alpha: int = 3
+ k: int = 8
+ t_expire: int = 86400
+ t_refresh: int = 900
+ t_replicate: int = 900
+
+
+@dataclass
+class DirConfig:
+ """
+ Directory configuration.
+ """
+
+ pol: DirectoryPolicy = DirectoryPolicy.DHT
+ dht: DhtConfig = field(default_factory=DhtConfig)
+
+
+@dataclass
+class UnicastConfig:
+ """
+ Configuration for unicast IPCPs.
+ """
+
+ dt: DtConfig = field(default_factory=DtConfig)
+ dir: DirConfig = field(default_factory=DirConfig)
+ addr_auth: AddressAuthPolicy = AddressAuthPolicy.FLAT_RANDOM
+ cong_avoid: CongestionAvoidPolicy = CongestionAvoidPolicy.MB_ECN
+
+
+@dataclass
+class EthConfig:
+ """
+ Configuration for Ethernet IPCPs (LLC or DIX).
+ """
+
+ dev: str = ""
+ ethertype: int = 0xA000
+
+
+@dataclass
+class Udp4Config:
+ """
+ Configuration for UDP over IPv4 IPCPs.
+ """
+
+ ip_addr: str = "0.0.0.0"
+ dns_addr: str = "0.0.0.0"
+ port: int = 3435
+
+
+@dataclass
+class Udp6Config:
+ """
+ Configuration for UDP over IPv6 IPCPs.
+ """
+
+ ip_addr: str = "::"
+ dns_addr: str = "::"
+ port: int = 3435
+
+
+@dataclass
+class IpcpConfig:
+ """
+ Configuration for bootstrapping an IPCP.
+ """
+
+ ipcp_type: IpcpType
+ layer_name: str = ""
+ dir_hash_algo: DirectoryHashAlgo = DirectoryHashAlgo.SHA3_256
+ unicast: Optional[UnicastConfig] = None
+ eth: Optional[EthConfig] = None
+ udp4: Optional[Udp4Config] = None
+ udp6: Optional[Udp6Config] = None
+
+
+@dataclass
+class NameSecPaths:
+ """
+ Security paths for a name.
+ """
+
+ sec: str = ""
+ key: str = ""
+ crt: str = ""
+
+
+@dataclass
+class NameInfo:
+ """
+ Information about a registered name.
+ """
+
+ name: str
+ pol_lb: LoadBalancePolicy = LoadBalancePolicy.ROUND_ROBIN
+ server_sec: NameSecPaths = field(default_factory=NameSecPaths)
+ client_sec: NameSecPaths = field(default_factory=NameSecPaths)
+
+
+@dataclass
+class IpcpInfo:
+ """
+ Information about a running IPCP.
+ """
+
+ pid: int
+ type: IpcpType
+ name: str
+ layer: str
+
+
+def _ipcp_config_to_c(conf: IpcpConfig) -> Any:
+ """
+ Convert an :class:`IpcpConfig` to a C ``struct ipcp_config *``.
+
+ The layer name is truncated to the 255 bytes the C struct holds.
+ """
+
+ _conf = ffi.new("struct ipcp_config *")
+
+ layer_name = conf.layer_name.encode()
+
+ ffi.memmove(_conf.layer_info.name, layer_name,
+ min(len(layer_name), 255))
+
+ _conf.layer_info.dir_hash_algo = conf.dir_hash_algo
+
+ _conf.type = conf.ipcp_type
+
+ if conf.ipcp_type == IpcpType.UNICAST:
+ uc = conf.unicast or UnicastConfig()
+ _conf.unicast.dt.addr_size = uc.dt.addr_size
+ _conf.unicast.dt.eid_size = uc.dt.eid_size
+ _conf.unicast.dt.max_ttl = uc.dt.max_ttl
+ _conf.unicast.dt.routing.pol = uc.dt.routing.pol
+ _conf.unicast.dt.routing.ls.pol = uc.dt.routing.ls.pol
+ _conf.unicast.dt.routing.ls.t_recalc = uc.dt.routing.ls.t_recalc
+ _conf.unicast.dt.routing.ls.t_update = uc.dt.routing.ls.t_update
+ _conf.unicast.dt.routing.ls.t_timeo = uc.dt.routing.ls.t_timeo
+ _conf.unicast.dir.pol = uc.dir.pol
+ _conf.unicast.dir.dht.params.alpha = uc.dir.dht.alpha
+ _conf.unicast.dir.dht.params.k = uc.dir.dht.k
+ _conf.unicast.dir.dht.params.t_expire = uc.dir.dht.t_expire
+ _conf.unicast.dir.dht.params.t_refresh = uc.dir.dht.t_refresh
+ _conf.unicast.dir.dht.params.t_replicate = uc.dir.dht.t_replicate
+ _conf.unicast.addr_auth_type = uc.addr_auth
+ _conf.unicast.cong_avoid = uc.cong_avoid
+
+ elif conf.ipcp_type in (IpcpType.ETH_LLC, IpcpType.ETH_DIX):
+ ec = conf.eth or EthConfig()
+ dev = ec.dev.encode()
+
+ ffi.memmove(_conf.eth.dev, dev, min(len(dev), 255))
+
+ _conf.eth.ethertype = ec.ethertype
+
+ elif conf.ipcp_type == IpcpType.UDP4:
+ uc4 = conf.udp4 or Udp4Config()
+ _conf.udp4.port = uc4.port
+ if lib.ipcp_config_udp4_set_ip(_conf, uc4.ip_addr.encode()) != 0:
+ raise ValueError(f"Invalid IPv4 address: {uc4.ip_addr}")
+
+ if lib.ipcp_config_udp4_set_dns(_conf, uc4.dns_addr.encode()) != 0:
+ raise ValueError(f"Invalid IPv4 DNS address: {uc4.dns_addr}")
+
+ elif conf.ipcp_type == IpcpType.UDP6:
+ uc6 = conf.udp6 or Udp6Config()
+ _conf.udp6.port = uc6.port
+ if lib.ipcp_config_udp6_set_ip(_conf, uc6.ip_addr.encode()) != 0:
+ raise ValueError(f"Invalid IPv6 address: {uc6.ip_addr}")
+
+ if lib.ipcp_config_udp6_set_dns(_conf, uc6.dns_addr.encode()) != 0:
+ raise ValueError(f"Invalid IPv6 DNS address: {uc6.dns_addr}")
+
+ return _conf
+
+
+def _name_info_to_c(info: NameInfo) -> Any:
+ """
+ Convert a :class:`NameInfo` to a C ``struct name_info *``.
+ """
+
+ _info = ffi.new("struct name_info *")
+
+ name = info.name.encode()
+
+ ffi.memmove(_info.name, name, min(len(name), 255))
+
+ _info.pol_lb = info.pol_lb
+
+ for attr, sec in (('s', info.server_sec), ('c', info.client_sec)):
+ sec_paths = getattr(_info, attr)
+
+ for fld in ('sec', 'key', 'crt'):
+ val = getattr(sec, fld).encode()
+
+ ffi.memmove(getattr(sec_paths, fld), val,
+ min(len(val), 511))
+
+ return _info
+
+
+def create_ipcp(name: str,
+ ipcp_type: IpcpType) -> int:
+ """
+ Create a new IPCP.
+
+ :param name: Name for the IPCP.
+ :param ipcp_type: Type of IPCP to create.
+ :return: PID of the created IPCP.
+ :raises IpcpCreateError: If creation fails or the IPCP is not found.
+ """
+
+ raise_errno(lib.irm_create_ipcp(name.encode(), ipcp_type),
+ default=IpcpCreateError)
+
+ for info in list_ipcps():
+ if info.name == name:
+ return info.pid
+
+ raise IpcpCreateError(f"IPCP '{name}' created but not found in list")
+
+
+def destroy_ipcp(pid: int) -> None:
+ """
+ Destroy an IPCP.
+
+ :param pid: PID of the IPCP to destroy.
+ :raises IrmError: If the IPCP could not be destroyed.
+ """
+
+ raise_errno(lib.irm_destroy_ipcp(pid), default=IrmError)
+
+
+def list_ipcps() -> List[IpcpInfo]:
+ """
+ List all running IPCPs.
+
+ :return: List of :class:`IpcpInfo` objects.
+ :raises IrmError: If the IPCPs could not be listed.
+ """
+
+ _ipcps = ffi.new("struct ipcp_list_info **")
+ n = raise_errno(lib.irm_list_ipcps(_ipcps), default=IrmError)
+
+ result = []
+
+ for i in range(n):
+ info = _ipcps[0][i]
+
+ result.append(IpcpInfo(
+ pid=info.pid,
+ type=IpcpType(info.type),
+ name=ffi.string(info.name).decode(),
+ layer=ffi.string(info.layer).decode(),
+ ))
+
+ if n > 0:
+ lib.free(_ipcps[0])
+
+ return result
+
+
+def enroll_ipcp(pid: int, dst: str) -> None:
+ """
+ Enroll an IPCP in a layer.
+
+ :param pid: PID of the IPCP to enroll.
+ :param dst: Name of a member of the layer to enroll with.
+ :raises IpcpEnrollError: If the IPCP could not enroll with *dst*.
+ """
+
+ raise_errno(lib.irm_enroll_ipcp(pid, dst.encode()),
+ default=IpcpEnrollError)
+
+
+def bootstrap_ipcp(pid: int, conf: IpcpConfig) -> None:
+ """
+ Bootstrap an IPCP.
+
+ :param pid: PID of the IPCP to bootstrap.
+ :param conf: Configuration for the IPCP.
+ :raises IpcpBootstrapError: If the IPCP could not be bootstrapped.
+ """
+
+ _conf = _ipcp_config_to_c(conf)
+
+ raise_errno(lib.irm_bootstrap_ipcp(pid, _conf),
+ default=IpcpBootstrapError)
+
+
+def connect_ipcp(pid: int,
+ component: str,
+ dst: str,
+ qos: Optional[QoSSpec] = None) -> None:
+ """
+ Connect an IPCP component to a destination.
+
+ :param pid: PID of the IPCP.
+ :param component: :data:`DT_COMP` or :data:`MGMT_COMP`.
+ :param dst: Destination name.
+ :param qos: QoS specification for the connection.
+ :raises IpcpConnectError: If *component* could not be
+ connected to *dst*.
+ """
+
+ _qos = qos_to_qosspec(ffi, qos)
+ if _qos == ffi.NULL:
+ _qos = ffi.new("qosspec_t *")
+
+ raise_errno(
+ lib.irm_connect_ipcp(pid, dst.encode(), component.encode(),
+ _qos[0]),
+ default=IpcpConnectError,
+ )
+
+
+def disconnect_ipcp(pid: int,
+ component: str,
+ dst: str) -> None:
+ """
+ Disconnect an IPCP component from a destination.
+
+ :param pid: PID of the IPCP.
+ :param component: :data:`DT_COMP` or :data:`MGMT_COMP`.
+ :param dst: Destination name.
+ :raises IpcpConnectError: If *component* could not be
+ disconnected from *dst*.
+ """
+
+ raise_errno(
+ lib.irm_disconnect_ipcp(pid, dst.encode(), component.encode()),
+ default=IpcpConnectError,
+ )
+
+
+def bind_program(prog: str,
+ name: str,
+ opts: int = 0,
+ argv: Optional[List[str]] = None) -> None:
+ """
+ Bind a program to a name.
+
+ :param prog: Path to the program.
+ :param name: Name to bind to.
+ :param opts: Bind options (e.g. :data:`BIND_AUTO`).
+ :param argv: Arguments passed when the IRM starts the program.
+ :raises BindError: If *prog* could not be bound to *name*.
+ """
+
+ if argv:
+ argc = len(argv)
+ _args = []
+
+ for a in argv:
+ _args.append(ffi.new("char[]", a.encode()))
+
+ _argv = ffi.new("char *[]", _args)
+ else:
+ argc = 0
+ _argv = ffi.NULL
+
+ raise_errno(
+ lib.irm_bind_program(prog.encode(), name.encode(),
+ opts, argc, _argv),
+ default=BindError,
+ )
+
+
+def unbind_program(prog: str, name: str) -> None:
+ """
+ Unbind a program from a name.
+
+ :param prog: Path to the program.
+ :param name: Name to unbind from.
+ :raises BindError: If *prog* could not be unbound from *name*.
+ """
+
+ raise_errno(lib.irm_unbind_program(prog.encode(), name.encode()),
+ default=BindError)
+
+
+def bind_process(pid: int, name: str) -> None:
+ """
+ Bind a running process to a name.
+
+ :param pid: PID of the process.
+ :param name: Name to bind to.
+ :raises BindError: If the process could not be bound to *name*.
+ """
+
+ raise_errno(lib.irm_bind_process(pid, name.encode()),
+ default=BindError)
+
+
+def unbind_process(pid: int, name: str) -> None:
+ """
+ Unbind a process from a name.
+
+ :param pid: PID of the process.
+ :param name: Name to unbind from.
+ :raises BindError: If the process could not be unbound from *name*.
+ """
+
+ raise_errno(lib.irm_unbind_process(pid, name.encode()),
+ default=BindError)
+
+
+def create_name(info: NameInfo) -> None:
+ """
+ Create a name in the IRM.
+
+ :param info: :class:`NameInfo` describing the name to create.
+ :raises NameExistsError: If the name already exists.
+ """
+
+ _info = _name_info_to_c(info)
+
+ raise_errno(lib.irm_create_name(_info), default=NameExistsError)
+
+
+def destroy_name(name: str) -> None:
+ """
+ Destroy a name in the IRM.
+
+ :param name: The name to destroy.
+ :raises NameNotFoundError: If the name is unknown.
+ """
+
+ raise_errno(lib.irm_destroy_name(name.encode()),
+ default=NameNotFoundError)
+
+
+def list_names() -> List[NameInfo]:
+ """
+ List all registered names.
+
+ :return: List of :class:`NameInfo` objects.
+ :raises IrmError: If the names could not be listed.
+ """
+
+ _names = ffi.new("struct name_info **")
+ n = raise_errno(lib.irm_list_names(_names), default=IrmError)
+
+ result = []
+
+ for i in range(n):
+ info = _names[0][i]
+
+ result.append(NameInfo(
+ name=ffi.string(info.name).decode(),
+ pol_lb=LoadBalancePolicy(info.pol_lb),
+ server_sec=NameSecPaths(
+ sec=ffi.string(info.s.sec).decode(),
+ key=ffi.string(info.s.key).decode(),
+ crt=ffi.string(info.s.crt).decode(),
+ ),
+ client_sec=NameSecPaths(
+ sec=ffi.string(info.c.sec).decode(),
+ key=ffi.string(info.c.key).decode(),
+ crt=ffi.string(info.c.crt).decode(),
+ ),
+ ))
+
+ if n > 0:
+ lib.free(_names[0])
+
+ return result
+
+
+def reg_name(name: str, pid: int) -> None:
+ """
+ Register an IPCP to a name.
+
+ :param name: The name to register.
+ :param pid: PID of the IPCP to register.
+ :raises NameNotFoundError: If the name is unknown.
+ """
+
+ raise_errno(lib.irm_reg_name(name.encode(), pid),
+ default=NameNotFoundError)
+
+
+def unreg_name(name: str, pid: int) -> None:
+ """
+ Unregister an IPCP from a name.
+
+ :param name: The name to unregister.
+ :param pid: PID of the IPCP to unregister.
+ :raises NameNotFoundError: If the name or the registration
+ is unknown.
+ """
+
+ raise_errno(lib.irm_unreg_name(name.encode(), pid),
+ default=NameNotFoundError)
diff --git a/ouroboros/qos.py b/ouroboros/qos.py
index f437ee2..c42a62e 100644
--- a/ouroboros/qos.py
+++ b/ouroboros/qos.py
@@ -1,110 +1,93 @@
#
-# Ouroboros - Copyright (C) 2016 - 2020
-#
-# Python API for applications - QoS
-#
-# 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
#
-from _ouroboros_cffi import ffi
-from math import modf
-from typing import Optional
+"""
+QoS specification for Ouroboros flows.
+"""
+
+from __future__ import annotations
-# Some constants
-MILLION = 1000 * 1000
-BILLION = 1000 * 1000 * 1000
+from dataclasses import dataclass
+from enum import IntEnum
+
+DEFAULT_PEER_TIMEOUT = 120000
+UINT32_MAX = 0xFFFFFFFF
+UINT64_MAX = 0xFFFFFFFFFFFFFFFF
+
+
+class QoSService(IntEnum):
+ """
+ Mirrors ``enum qos_service`` in ``ouroboros/qos.h``.
+ """
+ RAW = 0 # No FRCT; best-effort raw messages
+ MESSAGE = 1 # FRCT, reliable ordered messages
+ STREAM = 2 # FRCT, reliable ordered byte stream
+
+@dataclass(frozen=True)
class QoSSpec:
"""
- delay: In ms, default 1000s
- bandwidth: In bits / s, default 0
- availability: Class of 9s, default 0
- loss: Packet loss in ppm, default MILLION
- ber: Bit error rate, errors per billion bits. default BILLION
- in_order: In-order delivery, enables FRCT, default 0
- max_gap: Maximum interruption in ms, default MILLION
- cypher_s: Requested encryption strength in bits
+ QoS specification for a flow.
+
+ :ivar service: :class:`QoSService` (gates FRCT when > 0).
+ :ivar delay: Maximum one-way delay in ms.
+ :ivar bandwidth: Required bandwidth in bits/s.
+ :ivar availability: Class of 9s (number of nines of uptime).
+ :ivar loss: Maximum packet loss (per million).
+ :ivar ber: Maximum bit error rate (errors per billion).
+ :ivar max_gap: Maximum interruption in ms.
+ :ivar timeout: Peer timeout in ms (default 120000 ms).
"""
- def __init__(self,
- delay: int = MILLION,
- bandwidth: int = 0,
- availability: int = 0,
- loss: int = 1,
- ber: int = MILLION,
- in_order: int = 0,
- max_gap: int = MILLION,
- cypher_s: int = 0):
- self.delay = delay
- self.bandwidth = bandwidth
- self.availability = availability
- self.loss = loss
- self.ber = ber
- self.in_order = in_order
- self.max_gap = max_gap
- self.cypher_s = cypher_s
-
-
-def _fl_to_timespec(timeo: float):
- if timeo is None:
- return ffi.NULL
- elif timeo <= 0:
- return ffi.new("struct timespec *", [0, 0])
- else:
- frac, whole = modf(timeo)
- _timeo = ffi.new("struct timespec *")
- _timeo.tv_sec = whole
- _timeo.tv_nsec = frac * BILLION
- return _timeo
-
-
-def _timespec_to_fl(_timeo) -> Optional[float]:
- if _timeo is ffi.NULL:
- return None
- elif _timeo.tv_sec <= 0 and _timeo.tv_nsec == 0:
- return 0
- else:
- return _timeo.tv_sec + _timeo.tv_nsec / BILLION
-
-
-def _qos_to_qosspec(qos: QoSSpec):
- if qos is None:
- return ffi.NULL
- else:
- return ffi.new("qosspec_t *",
- [qos.delay,
- qos.bandwidth,
- qos.availability,
- qos.loss,
- qos.ber,
- qos.in_order,
- qos.max_gap,
- qos.cypher_s])
-
-
-def _qosspec_to_qos(_qos) -> Optional[QoSSpec]:
- if _qos is ffi.NULL:
- return None
- else:
- return QoSSpec(delay=_qos.delay,
- bandwidth=_qos.bandwidth,
- availability=_qos.availability,
- loss=_qos.loss,
- ber=_qos.ber,
- in_order=_qos.in_order,
- max_gap=_qos.max_gap,
- cypher_s=_qos.cypher_s)
+ service: QoSService = QoSService.RAW
+ delay: int = UINT32_MAX
+ bandwidth: int = 0
+ availability: int = 0
+ loss: int = 1
+ ber: int = 1
+ max_gap: int = UINT32_MAX
+ timeout: int = DEFAULT_PEER_TIMEOUT
+
+
+# Predefined QoS specs, mirroring the static const qosspec_t values in
+# ouroboros/qos.h. "_safe" sets ber=0 (integrity check); "rt" trades
+# reliability for latency.
+
+QOS_RAW = QoSSpec(
+ service=QoSService.RAW,
+ delay=UINT32_MAX, bandwidth=0, availability=0,
+ loss=1, ber=1,
+ max_gap=UINT32_MAX, timeout=0)
+
+QOS_RAW_SAFE = QoSSpec(
+ service=QoSService.RAW,
+ delay=UINT32_MAX, bandwidth=0, availability=0,
+ loss=1, ber=0,
+ max_gap=UINT32_MAX, timeout=0)
+
+QOS_RT = QoSSpec(
+ service=QoSService.MESSAGE,
+ delay=100, bandwidth=UINT64_MAX, availability=3,
+ loss=1, ber=1,
+ max_gap=100, timeout=DEFAULT_PEER_TIMEOUT)
+
+QOS_RT_SAFE = QoSSpec(
+ service=QoSService.MESSAGE,
+ delay=100, bandwidth=UINT64_MAX, availability=3,
+ loss=1, ber=0,
+ max_gap=100, timeout=DEFAULT_PEER_TIMEOUT)
+
+QOS_MSG = QoSSpec(
+ service=QoSService.MESSAGE,
+ delay=1000, bandwidth=0, availability=0,
+ loss=0, ber=0,
+ max_gap=2000, timeout=DEFAULT_PEER_TIMEOUT)
+
+QOS_STREAM = QoSSpec(
+ service=QoSService.STREAM,
+ delay=1000, bandwidth=0, availability=0,
+ loss=0, ber=0,
+ max_gap=2000, timeout=DEFAULT_PEER_TIMEOUT)