aboutsummaryrefslogtreecommitdiff
path: root/ouroboros
diff options
context:
space:
mode:
Diffstat (limited to 'ouroboros')
-rw-r--r--ouroboros/__init__.py36
-rw-r--r--ouroboros/_qosspec.py49
-rw-r--r--ouroboros/_timespec.py51
-rw-r--r--ouroboros/cli.py302
-rw-r--r--ouroboros/dev.py356
-rw-r--r--ouroboros/errors.py222
-rw-r--r--ouroboros/event.py114
-rw-r--r--ouroboros/irm.py292
-rw-r--r--ouroboros/qos.py37
9 files changed, 855 insertions, 604 deletions
diff --git a/ouroboros/__init__.py b/ouroboros/__init__.py
index a4de9bf..cbca95d 100644
--- a/ouroboros/__init__.py
+++ b/ouroboros/__init__.py
@@ -1,35 +1,15 @@
#
-# Ouroboros - Copyright (C) 2016 - 2026
-#
-# Python API for Ouroboros
-#
-# 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
#
-"""Public Python API for Ouroboros.
+"""
+Public Python API for Ouroboros.
-The package surface only re-exports pure-Python helpers (errors and
-QoS). Applications import the load-bearing parts of the API from
-their submodule explicitly: flows from :mod:`ouroboros.dev`, the
-event loop from :mod:`ouroboros.event`, and the control plane from
-:mod:`ouroboros.irm` (or :mod:`ouroboros.cli`). Submodule paths
-make the caller's intent explicit and keep the package importable
-in environments that have no business loading the libouroboros CFFI
-extensions (see ``CLAUDE.md`` for the underlying init-on-load
-behaviour that makes this contract load-bearing in practice).
+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
diff --git a/ouroboros/_qosspec.py b/ouroboros/_qosspec.py
index 6031c42..5c8b245 100644
--- a/ouroboros/_qosspec.py
+++ b/ouroboros/_qosspec.py
@@ -1,47 +1,35 @@
#
-# Ouroboros - Copyright (C) 2016 - 2026
-#
-# Internal helpers for translating QoSSpec <-> CFFI qosspec_t.
-#
-# 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
#
-"""Shared QoSSpec <-> qosspec_t conversion helpers.
+"""
+Shared QoSSpec <-> qosspec_t conversion helpers.
-dev.py and irm.py each link against their own CFFI extension module
-(``_ouroboros_dev_cffi`` and ``_ouroboros_irm_cffi``), so they each
-have their own ``ffi`` instance and their own ``qosspec_t`` type.
-CFFI types are not interchangeable across modules, so these helpers
-take the caller's ``ffi`` instance as a parameter.
+CFFI types are not interchangeable across extension modules, so the
+caller passes its own ``ffi`` instance.
"""
from __future__ import annotations
-from typing import Optional
+from typing import TYPE_CHECKING, Any, Optional
from ouroboros.qos import QoSSpec
+if TYPE_CHECKING:
+ from cffi import FFI
-def qos_to_qosspec(ffi, qos: Optional[QoSSpec]):
- """Convert a :class:`QoSSpec` to a freshly-allocated ``qosspec_t *``.
+
+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,
@@ -53,13 +41,16 @@ def qos_to_qosspec(ffi, qos: Optional[QoSSpec]):
qos.timeout])
-def qosspec_to_qos(ffi, _qos) -> Optional[QoSSpec]:
- """Convert a ``qosspec_t *`` back to a :class:`QoSSpec`.
+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,
diff --git a/ouroboros/_timespec.py b/ouroboros/_timespec.py
index 2fcf666..1656eb2 100644
--- a/ouroboros/_timespec.py
+++ b/ouroboros/_timespec.py
@@ -1,64 +1,59 @@
#
-# Ouroboros - Copyright (C) 2016 - 2026
-#
-# Internal helpers for translating float seconds <-> struct timespec.
-#
-# 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
#
-"""Shared float-seconds <-> ``struct timespec *`` conversions.
+"""
+Shared float-seconds <-> ``struct timespec *`` conversions.
-The dev and event modules both deal in seconds-as-float timeouts on
-the Python side and pass ``struct timespec *`` to the C library.
-The helpers take the caller's ``ffi`` instance as a parameter so the
-returned CData is bound to the right CFFI module.
+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 Optional
+from typing import TYPE_CHECKING, Any, Optional
+
+if TYPE_CHECKING:
+ from cffi import FFI
BILLION = 1000 * 1000 * 1000
-def fl_to_timespec(ffi, timeo: Optional[float]):
- """Convert *timeo* (seconds, or None) to a ``struct timespec *``.
+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, _timeo) -> Optional[float]:
- """Convert a ``struct timespec *`` to seconds-as-float.
+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
index 4b9ebb7..e887410 100644
--- a/ouroboros/cli.py
+++ b/ouroboros/cli.py
@@ -1,64 +1,13 @@
#
-# Ouroboros - Copyright (C) 2016 - 2026
-#
-# Python API for Ouroboros - CLI equivalents
-#
-# 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
#
-"""Higher-level wrappers that mirror the ``irm`` CLI tool behaviour.
-
-This is the Python counterpart of the C project's ``ouroboros/tools/irm``
-command-line tool. The ``irm`` CLI performs steps that the raw C
-library API does not, such as resolving program names via ``realpath``,
-looking up IPCP pids, and handling the ``autobind`` flag during
-bootstrap/enroll. This module exposes those same patterns as a Python
-API so callers do not need to re-implement them.
-
-Each wrapper corresponds to a specific ``irm`` sub-command:
-
-========================= ====================================
-Python CLI equivalent
-========================= ====================================
-``create_ipcp`` ``irm ipcp create``
-``destroy_ipcp`` ``irm ipcp destroy``
-``bootstrap_ipcp`` ``irm ipcp bootstrap [autobind]``
-``enroll_ipcp`` ``irm ipcp enroll [autobind]``
-``connect_ipcp`` ``irm ipcp connect``
-``disconnect_ipcp`` ``irm ipcp disconnect``
-``list_ipcps`` ``irm ipcp list``
-``bind_program`` ``irm bind program``
-``bind_process`` ``irm bind process``
-``bind_ipcp`` ``irm bind ipcp``
-``unbind_program`` ``irm unbind program``
-``unbind_process`` ``irm unbind process``
-``unbind_ipcp`` ``irm unbind ipcp``
-``create_name`` ``irm name create``
-``destroy_name`` ``irm name destroy``
-``reg_name`` ``irm name register``
-``unreg_name`` ``irm name unregister``
-``list_names`` ``irm name list``
-``autoboot`` ``irm ipcp bootstrap autobind``
- (with implicit ``create``)
-========================= ====================================
-
-Usage::
-
- from ouroboros.cli import create_ipcp, bootstrap_ipcp, enroll_ipcp
- from ouroboros.cli import bind_program, autoboot
+"""
+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
@@ -110,7 +59,7 @@ __all__ = [
"unbind_program", "unbind_process", "unbind_ipcp",
"create_name", "destroy_name", "reg_name", "unreg_name", "list_names",
"autoboot",
- # Re-exported configuration types (convenience)
+ # Re-exported configuration types
"DhtConfig", "DirConfig", "DtConfig", "EthConfig",
"IpcpConfig", "IpcpInfo", "IpcpType",
"LinkStateConfig", "NameInfo", "RoutingConfig",
@@ -119,10 +68,14 @@ __all__ = [
def _pid_of(ipcp_name: str) -> int:
- """Look up the pid of a running IPCP by its name."""
+ """
+ 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}")
@@ -130,68 +83,83 @@ 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."""
+ """
+ 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.
-
- Mirrors ``irm ipcp destroy name <name>``.
-
- Resolves the IPCP name to a pid, then destroys the IPCP.
+ """
+ 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.
+ """
+ List running IPCPs, optionally filtered.
- Mirrors ``irm ipcp list [name <n>] [type <t>] [layer <l>]``.
+ 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
@@ -200,21 +168,10 @@ def reg_name(name: str,
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.
-
- Mirrors ``irm name register <name> ipcp <ipcp> [ipcp ...]
- layer <layer> [layer ...]``.
-
- The C CLI tool resolves IPCP names and layer names to pids,
- checks whether the name already exists and calls
- ``irm_create_name`` before ``irm_reg_name`` for each IPCP.
-
- The function accepts flexible input:
+ """
+ Register a name with IPCP(s), creating it first if needed.
- - A single *ipcp* name or list of *ipcps* names.
- - A single *layer* name or list of *layers* names (registers
- with every IPCP in each of those layers).
- - Any combination of the above.
+ Registers with every IPCP in *layer* and *layers*.
:param name: The name to register.
:param ipcp: Single IPCP name to register with.
@@ -222,6 +179,7 @@ def reg_name(name: str,
: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))
@@ -234,129 +192,120 @@ def bind_program(prog: str,
name: str,
opts: int = 0,
argv: Optional[List[str]] = None) -> None:
- """Bind a program to a name, resolving bare names to full paths.
-
- Mirrors ``irm bind program <prog> name <name>``.
-
- The ``irm bind program`` CLI tool calls ``realpath()`` on *prog*
- before passing it to the library. The raw C function
- ``irm_bind_program`` contains a ``check_prog_path`` helper that
- corrupts the ``PATH`` environment variable (writes NUL over ``:``
- separators) when given a bare program name. Only the first such
- call would succeed in a long-running process.
-
- This wrapper resolves *prog* via ``shutil.which()`` before calling
- the library, avoiding the bug entirely.
+ """
+ Bind a program to a name.
:param prog: Program name or path. Bare names (without ``/``) are
- resolved on ``PATH`` via ``shutil.which()``.
+ resolved on ``PATH``.
:param name: Name to bind to.
:param opts: Bind options (e.g. ``BIND_AUTO``).
- :param argv: Arguments to pass when the program is auto-started.
- :raises BindError: If the program cannot be found or the bind
- call fails.
+ :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.
-
- Mirrors ``irm unbind program <prog> name <name>``.
+ """
+ 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.
-
- Mirrors ``irm bind process <pid> name <name>``.
+ """
+ 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.
-
- Mirrors ``irm unbind process <pid> name <name>``.
+ """
+ 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.
-
- Mirrors ``irm bind ipcp <ipcp> name <name>``.
-
- Resolves the IPCP name to a pid, then calls :func:`bind_process`.
+ """
+ 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.
-
- Mirrors ``irm unbind ipcp <ipcp> name <name>``.
-
- Resolves the IPCP name to a pid, then calls :func:`unbind_process`.
+ """
+ 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.
-
- Mirrors ``irm name create <name> [lb <policy>]``.
+ """
+ Create a registered name.
:param name: The name to create.
- :param pol_lb: Load-balance policy (optional).
- :param info: Full :class:`NameInfo` (overrides *name* / *pol_lb*
- if given).
+ :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.
-
- Mirrors ``irm name list [<name>]``.
+ """
+ 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
@@ -365,12 +314,10 @@ def unreg_name(name: str,
ipcps: Optional[List[str]] = None,
layer: Optional[str] = None,
layers: Optional[List[str]] = None) -> None:
- """Unregister a name from IPCP(s).
-
- Mirrors ``irm name unregister <name> ipcp <ipcp> [ipcp ...]
- layer <layer> [layer ...]``.
+ """
+ Unregister a name from IPCP(s).
- Accepts the same flexible input as :func:`reg_name`.
+ Unregisters from every IPCP in *layer* and *layers*.
:param name: The name to unregister.
:param ipcp: Single IPCP name to unregister from.
@@ -378,6 +325,7 @@ def unreg_name(name: str,
: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)
@@ -385,28 +333,21 @@ def unreg_name(name: str,
def bootstrap_ipcp(name: str,
conf: IpcpConfig,
autobind: bool = False) -> None:
- """Bootstrap an IPCP, optionally binding it to its name and layer.
-
- Mirrors ``irm ipcp bootstrap name <n> layer <l> [autobind]``.
-
- When *autobind* is ``True`` and the IPCP type is ``UNICAST`` or
- ``BROADCAST``, the sequence is::
-
- bind_process(pid, ipcp_name) # accept flows for ipcp name
- bind_process(pid, layer_name) # accept flows for layer name
- bootstrap_ipcp(pid, conf) # bootstrap into the layer
+ """
+ Bootstrap an IPCP, optionally binding it to its name and layer.
- This matches the C ``irm ipcp bootstrap`` tool exactly. If
- bootstrap fails after autobind, the bindings are rolled back.
+ 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 (includes layer name & type).
+ :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)
+ 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)
@@ -417,33 +358,26 @@ def bootstrap_ipcp(name: str,
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.
-
- Mirrors ``irm ipcp enroll name <n> layer <dst> [autobind]``.
-
- When *autobind* is ``True``, the sequence is::
-
- enroll_ipcp(pid, dst)
- bind_process(pid, ipcp_name)
- bind_process(pid, layer_name) # layer learned from enrollment
-
- This matches the C ``irm ipcp enroll`` tool exactly.
+ """
+ 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 layer
- after successful enrollment.
+ :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:
- # Look up enrolled layer from the IPCP list.
for info in _irm_list_ipcps():
if info.pid == pid:
_irm_bind_process(pid, info.name)
@@ -453,44 +387,38 @@ def enroll_ipcp(name: str, dst: str,
def connect_ipcp(name: str, dst: str, comp: str = "*",
qos: Optional[QoSSpec] = None) -> None:
- """Connect IPCP components to a destination.
-
- Mirrors ``irm ipcp connect name <n> dst <dst> [component <c>]
- [qos <qos>]``.
-
- When *comp* is ``"*"`` (default), both ``dt`` and ``mgmt``
- components are connected, matching the CLI default.
+ """
+ Connect IPCP components to a destination.
:param name: Name of the IPCP.
:param dst: Destination IPCP name.
- :param comp: Component to connect: ``"dt"``, ``"mgmt"``, or
- ``"*"`` for both (default).
+ :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.
-
- Mirrors ``irm ipcp disconnect name <n> dst <dst>
- [component <c>]``.
-
- When *comp* is ``"*"`` (default), both ``dt`` and ``mgmt``
- components are disconnected, matching the CLI default.
+ """
+ Disconnect IPCP components from a destination.
:param name: Name of the IPCP.
:param dst: Destination IPCP name.
- :param comp: Component to disconnect: ``"dt"``, ``"mgmt"``, or
- ``"*"`` for both (default).
+ :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)
@@ -499,24 +427,22 @@ def autoboot(name: str,
ipcp_type: IpcpType,
layer: str,
conf: Optional[IpcpConfig] = None) -> None:
- """Create, autobind and bootstrap an IPCP in one step.
-
- Convenience wrapper equivalent to::
-
- irm ipcp bootstrap name <name> type <type> layer <layer> autobind
-
- (with an implicit ``create`` if the IPCP does not yet exist).
+ """
+ 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: Optional IPCP configuration. If *None*, a default
- :class:`IpcpConfig` is created for the given
- *ipcp_type* and *layer*.
+ :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 ffa59b6..1397bca 100644
--- a/ouroboros/dev.py
+++ b/ouroboros/dev.py
@@ -1,31 +1,17 @@
#
-# Ouroboros - Copyright (C) 2016 - 2026
-#
-# Python API for Ouroboros
-#
-# 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
#
-"""Flow allocation, acceptance and I/O for the Ouroboros dev API."""
+"""
+Flow allocation, acceptance and I/O for the Ouroboros dev API.
+"""
from __future__ import annotations
import warnings
from enum import IntFlag
-from typing import Optional
+from typing import TYPE_CHECKING, Optional, Type
from _ouroboros_dev_cffi import ffi, lib
@@ -40,19 +26,28 @@ from ouroboros.errors import (
)
from ouroboros.qos import QoSSpec
+if TYPE_CHECKING:
+ from types import TracebackType
+
check_ouroboros_version(lib.OUROBOROS_VERSION_MAJOR,
lib.OUROBOROS_VERSION_MINOR)
class FrctFlags(IntFlag):
- """FRCT-level feature flags."""
+ """
+ FRCT-level feature flags.
+ """
+
RETRANSMIT = 0o1
RESCNTL = 0o2
LINGER = 0o4
class FlowProperties(IntFlag):
- """Flags describing flow properties and blocking behaviour."""
+ """
+ Flags describing flow properties and blocking behaviour.
+ """
+
READ_ONLY = 0o0
WRITE_ONLY = 0o1
READ_WRITE = 0o2
@@ -65,47 +60,68 @@ class FlowProperties(IntFlag):
class Flow:
- """Represents an allocated Ouroboros flow."""
+ """
+ Represents an allocated Ouroboros flow.
+ """
def __init__(self, fd: int = -1) -> None:
- """Construct a Flow wrapper.
+ """
+ Construct a Flow wrapper.
- :param fd: Existing flow descriptor to wrap, or -1 for an
- unallocated Flow that will be populated by
- :meth:`alloc`, :meth:`accept` or :meth:`join`.
+ :param fd: Existing flow descriptor, or -1 for an unallocated
+ flow.
"""
+
self._fd: int = fd
def __enter__(self) -> Flow:
return self
- def __exit__(self, exc_type, exc_value, tb) -> None:
+ def __exit__(self, exc_type: Optional[Type[BaseException]],
+ exc_value: Optional[BaseException],
+ tb: Optional[TracebackType]) -> None:
self.dealloc()
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 # interpreter shutdown may have torn down lib already
+ pass
def __repr__(self) -> str:
return f"Flow(fd={self._fd})"
def fileno(self) -> int:
- """Return the underlying ouroboros flow descriptor."""
+ """
+ Return the underlying ouroboros flow descriptor.
+
+ :return: The ouroboros flow descriptor, or -1 if unallocated.
+ """
+
return self._fd
def alloc(self,
dst: str,
qos: Optional[QoSSpec] = None,
timeo: Optional[float] = None) -> Optional[QoSSpec]:
- """Allocate a flow with a certain QoS to a destination.
+ """
+ Allocate a flow with a certain QoS to a destination.
:param dst: Destination name.
:param qos: Requested QoS.
- :param timeo: Allocation timeout (None blocks forever, 0 is async).
+ :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 FlowAlreadyAllocatedError()
@@ -113,18 +129,25 @@ class Flow:
_timeo = fl_to_timespec(ffi, timeo)
rc = lib.flow_alloc(dst.encode(), _qos, _timeo)
+
raise_errno(rc)
+
self._fd = rc
return qosspec_to_qos(ffi, _qos)
def accept(self,
timeo: Optional[float] = None) -> Optional[QoSSpec]:
- """Accept an incoming flow and return its QoS.
+ """
+ Accept an incoming flow and return its QoS.
: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 FlowAlreadyAllocatedError()
@@ -132,7 +155,9 @@ class Flow:
_timeo = fl_to_timespec(ffi, timeo)
rc = lib.flow_accept(_qos, _timeo)
+
raise_errno(rc)
+
self._fd = rc
return qosspec_to_qos(ffi, _qos)
@@ -140,26 +165,38 @@ class Flow:
def join(self,
dst: str,
timeo: Optional[float] = None) -> None:
- """Join a broadcast layer.
+ """
+ Join a broadcast layer.
- :param dst: Destination broadcast layer name.
+ :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 FlowAlreadyAllocatedError()
_timeo = fl_to_timespec(ffi, timeo)
rc = lib.flow_join(dst.encode(), _timeo)
+
raise_errno(rc)
+
self._fd = rc
def dealloc(self) -> None:
- """Deallocate this flow. Idempotent: a no-op on an unallocated flow."""
+ """
+ Deallocate this flow; a no-op on an unallocated flow.
+ """
+
if self._fd < 0:
return
+
rc = lib.flow_dealloc(self._fd)
self._fd = -1
+
if rc < 0:
warnings.warn(f"flow_dealloc returned {rc}",
FlowDeallocWarning, stacklevel=2)
@@ -167,13 +204,17 @@ class Flow:
def write(self,
buf: bytes,
count: Optional[int] = None) -> int:
- """Write up to *count* bytes to the flow.
+ """
+ Write up to *count* bytes to the flow.
:param buf: Buffer to write from.
- :param count: Number of bytes to write (defaults to ``len(buf)``).
+ :param count: Number of bytes to write (defaults to
+ ``len(buf)``).
:return: Number of bytes written.
- :raises FlowError: (or subclass) on negative ouroboros return codes.
+ :raises FlowError: On a negative ouroboros return code.
+ :raises FlowNotAllocatedError: If the flow is not allocated.
"""
+
if self._fd < 0:
raise FlowNotAllocatedError()
@@ -181,30 +222,38 @@ class Flow:
count = len(buf)
rc = lib.flow_write(self._fd, ffi.from_buffer(buf), count)
+
return raise_errno(rc)
def writeline(self,
ln: str) -> int:
- """Encode *ln* as UTF-8 and write it to the flow.
+ """
+ Encode *ln* as UTF-8 and write it to the flow.
:param ln: String to write.
:return: Number of bytes written.
+ :raises FlowNotAllocatedError: If the flow is not allocated.
"""
+
if self._fd < 0:
raise FlowNotAllocatedError()
data = ln.encode()
+
return self.write(data, len(data))
def read(self,
count: Optional[int] = None) -> bytes:
- """Read up to *count* bytes from the flow.
+ """
+ Read up to *count* bytes from the flow.
:param count: Maximum number of bytes to read (default 2048).
- :return: Bytes read (may be empty when an SDU was just
- fully consumed by a previous matching read).
- :raises FlowError: (or subclass) on negative ouroboros return codes.
+ :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 FlowNotAllocatedError()
@@ -214,181 +263,320 @@ class Flow:
_buf = ffi.new("char []", count)
rc = lib.flow_read(self._fd, _buf, count)
+
raise_errno(rc)
return ffi.unpack(_buf, rc)
def readline(self) -> str:
- """Read from the flow and decode the result as UTF-8."""
+ """
+ Read from the flow and decode the result as UTF-8.
+
+ :return: Decoded UTF-8 string read from the flow.
+ :raises FlowNotAllocatedError: If the flow is not allocated.
+ """
+
if self._fd < 0:
raise FlowNotAllocatedError()
return self.read().decode()
def set_snd_timeout(self, timeo: float) -> None:
- """Set the timeout for blocking writes (seconds)."""
+ """
+ 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(ffi, timeo)
+
raise_errno(lib.flow_set_snd_timeout(self._fd, _timeo))
def get_snd_timeout(self) -> Optional[float]:
- """Return the timeout for blocking writes (seconds)."""
+ """
+ Return the timeout for blocking writes (seconds).
+
+ :return: Timeout for blocking writes, in seconds.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
_timeo = ffi.new("struct timespec *")
+
raise_errno(lib.flow_get_snd_timeout(self._fd, _timeo))
+
return timespec_to_fl(ffi, _timeo)
def set_rcv_timeout(self, timeo: float) -> None:
- """Set the timeout for blocking reads (seconds)."""
+ """
+ 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(ffi, timeo)
+
raise_errno(lib.flow_set_rcv_timeout(self._fd, _timeo))
def get_rcv_timeout(self) -> Optional[float]:
- """Return the timeout for blocking reads (seconds)."""
+ """
+ Return the timeout for blocking reads (seconds).
+
+ :return: Timeout for blocking reads, in seconds.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
_timeo = ffi.new("struct timespec *")
+
raise_errno(lib.flow_get_rcv_timeout(self._fd, _timeo))
+
return timespec_to_fl(ffi, _timeo)
def get_qos(self) -> Optional[QoSSpec]:
- """Return the current QoS in effect on the flow."""
+ """
+ Return the current QoS in effect 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 *")
+
raise_errno(lib.flow_get_qos(self._fd, _qos))
+
return qosspec_to_qos(ffi, _qos)
def get_rx_queue_len(self) -> int:
- """Return the receive queue length (bytes)."""
+ """
+ Return the receive queue length (bytes).
+
+ :return: Receive queue length in bytes.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
size = ffi.new("size_t *")
+
raise_errno(lib.flow_get_rx_qlen(self._fd, size))
+
return int(size[0])
def get_tx_queue_len(self) -> int:
- """Return the transmit queue length (bytes)."""
+ """
+ Return the transmit queue length (bytes).
+
+ :return: Transmit queue length in bytes.
+ :raises FlowError: On a negative ouroboros return code.
+ """
+
size = ffi.new("size_t *")
+
raise_errno(lib.flow_get_tx_qlen(self._fd, size))
+
return int(size[0])
def get_mtu(self) -> int:
- """Return the per-packet MTU.
+ """
+ Return the maximum user payload that fits in one n-1 PDU after
+ crypto headers (bytes), or 0 if unknown.
- This is the maximum user payload that fits in one n-1 PDU,
- after crypto headers. Returns 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.
-
- .. warning::
- This OVERWRITES the entire ``oflags`` value, including
- the access-mode bits (``READ_ONLY`` / ``WRITE_ONLY`` /
- ``READ_WRITE``). Passing only ``NO_PARTIAL_READ`` (for
- example) silently downgrades the flow to read-only and
- subsequent writes will fail with :class:`FlowPermissionError`.
-
- To preserve the existing access mode use
- :meth:`add_flags` / :meth:`remove_flags`, or read the
- current flags with :meth:`get_flags` and OR in the new
- bits explicitly.
+ """
+ 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 ``oflags`` value.
+ """
+ OR *flags* into the current flags, leaving other bits set.
- Preserves the access mode and any other already-set bits.
+ :param flags: :class:`FlowProperties` bits to set.
+ :raises FlowError: On a negative ouroboros return code.
"""
+
current = self.get_flags()
+
self.set_flags(current | FlowProperties(int(flags)))
def remove_flags(self, flags: FlowProperties) -> None:
- """Clear *flags* from the current ``oflags`` value.
+ """
+ Clear *flags* from the current flags, leaving other bits set.
- Preserves every other bit (including the access mode).
+ :param flags: :class:`FlowProperties` bits to clear.
+ :raises FlowError: On a negative ouroboros return code.
"""
+
current = self.get_flags()
+
self.set_flags(current & ~FlowProperties(int(flags)))
def get_flags(self) -> FlowProperties:
- """Return the current 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 = 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.
+ """
+ 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 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)."""
+ """
+ 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 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)."""
+ """
+ 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 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)."""
+ """
+ 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 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: Optional[QoSSpec] = None,
timeo: Optional[float] = None) -> Flow:
- """Allocate a new flow and return the :class:`Flow` wrapper.
+ """
+ Allocate a new flow and return the :class:`Flow` wrapper.
: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: Optional[float] = None) -> Flow:
- """Accept an incoming flow and return the :class:`Flow` wrapper.
+ """
+ Accept an incoming flow and return the :class:`Flow` wrapper.
: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,
timeo: Optional[float] = None) -> Flow:
- """Join a broadcast layer and return the :class:`Flow` wrapper.
+ """
+ Join a broadcast layer and return the :class:`Flow` wrapper.
: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, timeo)
+
return f
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(
diff --git a/ouroboros/event.py b/ouroboros/event.py
index aa03ad4..24b58b8 100644
--- a/ouroboros/event.py
+++ b/ouroboros/event.py
@@ -1,30 +1,16 @@
#
-# Ouroboros - Copyright (C) 2016 - 2026
-#
-# Python API for Ouroboros
-#
-# 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
#
-"""Asynchronous flow event monitoring for Ouroboros."""
+"""
+Asynchronous flow event monitoring for Ouroboros.
+"""
from __future__ import annotations
from enum import IntFlag
-from typing import List, Optional, Tuple
+from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Type
from _ouroboros_dev_cffi import ffi, lib
@@ -32,9 +18,15 @@ 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):
- """Types of flow events."""
+ """
+ Types of flow events.
+ """
+
FLOW_PKT = lib.FLOW_PKT
FLOW_DOWN = lib.FLOW_DOWN
FLOW_UP = lib.FLOW_UP
@@ -44,7 +36,9 @@ class FEventType(IntFlag):
class FEventQueue:
- """A queue of flow events waiting to be drained."""
+ """
+ A queue of flow events waiting to be drained.
+ """
def __init__(self) -> None:
self._fq = lib.fqueue_create()
@@ -54,40 +48,58 @@ class FEventQueue:
def __enter__(self) -> FEventQueue:
return self
- def __exit__(self, exc_type, exc_value, tb) -> None:
+ 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 destroy(self) -> None:
- """Destroy the underlying queue. Idempotent."""
+ """
+ Destroy the underlying queue. Idempotent.
+ """
+
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 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.
+ """
+
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):
- """Return the underlying ``fqueue_t *``.
+ def handle(self) -> Any:
+ """
+ Return the underlying ``fqueue_t *``.
- Intended for use by :meth:`FlowSet.wait` within the same
- package; not part of the user-facing API.
+ :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: Optional[List[Flow]] = None) -> None:
self._set = lib.fset_create()
@@ -98,26 +110,40 @@ class FlowSet:
for flow in flows:
if lib.fset_add(self._set, flow.fileno()) != 0:
lib.fset_destroy(self._set)
+
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) -> None:
+ 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 destroy(self) -> None:
- """Destroy the underlying set. Idempotent."""
+ """
+ Destroy the underlying set. Idempotent.
+ """
+
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."""
+ """
+ Add *flow* to this set.
+
+ :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")
@@ -125,14 +151,25 @@ class FlowSet:
raise MemoryError(f"Failed to add flow {flow.fileno()}")
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 == ffi.NULL:
raise ValueError("FlowSet has been destroyed")
lib.fset_zero(self._set)
def remove(self, flow: Flow) -> None:
- """Remove *flow* from this set."""
+ """
+ Remove *flow* from this set.
+
+ :param flow: The flow to remove from this set.
+ :raises ValueError: If the FlowSet has been destroyed.
+ """
+
if self._set == ffi.NULL:
raise ValueError("FlowSet has been destroyed")
@@ -141,15 +178,20 @@ class FlowSet:
def wait(self,
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 == ffi.NULL:
raise ValueError("FlowSet has been destroyed")
_timeo = fl_to_timespec(ffi, timeo)
rc = lib.fevent(self._set, fq.handle, _timeo)
+
raise_errno(rc, default=FlowEventError)
diff --git a/ouroboros/irm.py b/ouroboros/irm.py
index 34a65fb..ea7d665 100644
--- a/ouroboros/irm.py
+++ b/ouroboros/irm.py
@@ -1,31 +1,17 @@
#
-# Ouroboros - Copyright (C) 2016 - 2026
-#
-# Python API for Ouroboros - IRM
-#
-# 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
#
-"""IRM (IPC Resource Manager) bindings for Ouroboros."""
+"""
+IRM (IPC Resource Manager) bindings for Ouroboros.
+"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import IntEnum
-from typing import List, Optional
+from typing import Any, List, Optional
from _ouroboros_irm_cffi import ffi, lib
@@ -47,11 +33,17 @@ 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"
-# --- Enumerations ---
class IpcpType(IntEnum):
- """IPCP types available in Ouroboros."""
+ """
+ IPCP types available in Ouroboros.
+ """
+
LOCAL = lib.IPCP_LOCAL
UNICAST = lib.IPCP_UNICAST
BROADCAST = lib.IPCP_BROADCAST
@@ -62,35 +54,53 @@ class IpcpType(IntEnum):
class AddressAuthPolicy(IntEnum):
- """Address authority policies for unicast IPCPs."""
+ """
+ Address authority policies for unicast IPCPs.
+ """
+
FLAT_RANDOM = lib.ADDR_AUTH_FLAT_RANDOM
class LinkStatePolicy(IntEnum):
- """Link state routing policies."""
+ """
+ Link state routing policies.
+ """
+
SIMPLE = lib.LS_SIMPLE
LFA = lib.LS_LFA
ECMP = lib.LS_ECMP
class RoutingPolicy(IntEnum):
- """Routing policies."""
+ """
+ Routing policies.
+ """
+
LINK_STATE = lib.ROUTING_LINK_STATE
class CongestionAvoidPolicy(IntEnum):
- """Congestion avoidance policies."""
+ """
+ Congestion avoidance policies.
+ """
+
NONE = lib.CA_NONE
MB_ECN = lib.CA_MB_ECN
class DirectoryPolicy(IntEnum):
- """Directory policies."""
+ """
+ Directory policies.
+ """
+
DHT = lib.DIR_DHT
class DirectoryHashAlgo(IntEnum):
- """Directory hash algorithms."""
+ """
+ Directory hash algorithms.
+ """
+
SHA3_224 = lib.DIR_HASH_SHA3_224
SHA3_256 = lib.DIR_HASH_SHA3_256
SHA3_384 = lib.DIR_HASH_SHA3_384
@@ -98,23 +108,20 @@ class DirectoryHashAlgo(IntEnum):
class LoadBalancePolicy(IntEnum):
- """Load balancing policies for names."""
+ """
+ Load balancing policies for names.
+ """
+
ROUND_ROBIN = lib.LB_RR
SPILL = lib.LB_SPILL
-BIND_AUTO = lib.BIND_AUTO
-
-# Unicast IPCP component names
-DT_COMP = "Data Transfer"
-MGMT_COMP = "Management"
-
-
-# --- Configuration classes ---
-
@dataclass
class LinkStateConfig:
- """Configuration for link state routing."""
+ """
+ Configuration for link state routing.
+ """
+
pol: LinkStatePolicy = LinkStatePolicy.SIMPLE
t_recalc: int = 4
t_update: int = 15
@@ -123,14 +130,20 @@ class LinkStateConfig:
@dataclass
class RoutingConfig:
- """Routing configuration."""
+ """
+ Routing configuration.
+ """
+
pol: RoutingPolicy = RoutingPolicy.LINK_STATE
ls: LinkStateConfig = field(default_factory=LinkStateConfig)
@dataclass
class DtConfig:
- """Data transfer configuration for unicast IPCPs."""
+ """
+ Data transfer configuration for unicast IPCPs.
+ """
+
addr_size: int = 4
eid_size: int = 8
max_ttl: int = 60
@@ -139,7 +152,10 @@ class DtConfig:
@dataclass
class DhtConfig:
- """DHT directory configuration."""
+ """
+ DHT directory configuration.
+ """
+
alpha: int = 3
k: int = 8
t_expire: int = 86400
@@ -149,14 +165,20 @@ class DhtConfig:
@dataclass
class DirConfig:
- """Directory configuration."""
+ """
+ Directory configuration.
+ """
+
pol: DirectoryPolicy = DirectoryPolicy.DHT
dht: DhtConfig = field(default_factory=DhtConfig)
@dataclass
class UnicastConfig:
- """Configuration for unicast IPCPs."""
+ """
+ Configuration for unicast IPCPs.
+ """
+
dt: DtConfig = field(default_factory=DtConfig)
dir: DirConfig = field(default_factory=DirConfig)
addr_auth: AddressAuthPolicy = AddressAuthPolicy.FLAT_RANDOM
@@ -165,14 +187,20 @@ class UnicastConfig:
@dataclass
class EthConfig:
- """Configuration for Ethernet IPCPs (LLC or DIX)."""
+ """
+ Configuration for Ethernet IPCPs (LLC or DIX).
+ """
+
dev: str = ""
ethertype: int = 0xA000
@dataclass
class Udp4Config:
- """Configuration for UDP over IPv4 IPCPs."""
+ """
+ Configuration for UDP over IPv4 IPCPs.
+ """
+
ip_addr: str = "0.0.0.0"
dns_addr: str = "0.0.0.0"
port: int = 3435
@@ -180,7 +208,10 @@ class Udp4Config:
@dataclass
class Udp6Config:
- """Configuration for UDP over IPv6 IPCPs."""
+ """
+ Configuration for UDP over IPv6 IPCPs.
+ """
+
ip_addr: str = "::"
dns_addr: str = "::"
port: int = 3435
@@ -188,18 +219,10 @@ class Udp6Config:
@dataclass
class IpcpConfig:
- """Configuration for bootstrapping an IPCP.
-
- Depending on the IPCP type, set the appropriate sub-configuration:
-
- - UNICAST: ``unicast`` (:class:`UnicastConfig`)
- - ETH_LLC: ``eth`` (:class:`EthConfig`)
- - ETH_DIX: ``eth`` (:class:`EthConfig`)
- - UDP4: ``udp4`` (:class:`Udp4Config`)
- - UDP6: ``udp6`` (:class:`Udp6Config`)
- - LOCAL: no extra config needed
- - BROADCAST: no extra config needed
"""
+ Configuration for bootstrapping an IPCP.
+ """
+
ipcp_type: IpcpType
layer_name: str = ""
dir_hash_algo: DirectoryHashAlgo = DirectoryHashAlgo.SHA3_256
@@ -211,7 +234,10 @@ class IpcpConfig:
@dataclass
class NameSecPaths:
- """Security paths for a name (security config, key, certificate)."""
+ """
+ Security paths for a name.
+ """
+
sec: str = ""
key: str = ""
crt: str = ""
@@ -219,7 +245,10 @@ class NameSecPaths:
@dataclass
class NameInfo:
- """Information about a registered name."""
+ """
+ Information about a registered name.
+ """
+
name: str
pol_lb: LoadBalancePolicy = LoadBalancePolicy.ROUND_ROBIN
server_sec: NameSecPaths = field(default_factory=NameSecPaths)
@@ -228,23 +257,30 @@ class NameInfo:
@dataclass
class IpcpInfo:
- """Information about a running IPCP (from list_ipcps)."""
+ """
+ Information about a running IPCP.
+ """
+
pid: int
type: IpcpType
name: str
layer: str
-# --- Internal conversion functions ---
+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.
+ """
-def _ipcp_config_to_c(conf: IpcpConfig):
- """Convert an :class:`IpcpConfig` to a C ``struct ipcp_config *``."""
_conf = ffi.new("struct ipcp_config *")
- # Layer info
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
@@ -271,7 +307,9 @@ def _ipcp_config_to_c(conf: IpcpConfig):
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:
@@ -279,6 +317,7 @@ def _ipcp_config_to_c(conf: IpcpConfig):
_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}")
@@ -287,45 +326,52 @@ def _ipcp_config_to_c(conf: IpcpConfig):
_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):
- """Convert a :class:`NameInfo` to a C ``struct name_info *``."""
+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
-# --- IRM API functions ---
-
def create_ipcp(name: str,
ipcp_type: IpcpType) -> int:
- """Create a new IPCP.
+ """
+ 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)
- # The C function returns 0 on success, not the pid.
- # Look up the actual pid by name.
for info in list_ipcps():
if info.name == name:
return info.pid
@@ -334,24 +380,32 @@ def create_ipcp(name: str,
def destroy_ipcp(pid: int) -> None:
- """Destroy an IPCP.
+ """
+ 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.
+ """
+ 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),
@@ -366,22 +420,29 @@ def list_ipcps() -> List[IpcpInfo]:
def enroll_ipcp(pid: int, dst: str) -> None:
- """Enroll an IPCP in a layer.
+ """
+ Enroll an IPCP in a layer.
:param pid: PID of the IPCP to enroll.
- :param dst: Name to use for enrollment.
+ :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.
+ """
+ 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)
@@ -390,17 +451,21 @@ def connect_ipcp(pid: int,
component: str,
dst: str,
qos: Optional[QoSSpec] = None) -> None:
- """Connect an IPCP component to a destination.
+ """
+ Connect an IPCP component to a destination.
:param pid: PID of the IPCP.
- :param component: Component to connect (:data:`DT_COMP` or
- :data:`MGMT_COMP`).
+ :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]),
@@ -411,12 +476,16 @@ def connect_ipcp(pid: int,
def disconnect_ipcp(pid: int,
component: str,
dst: str) -> None:
- """Disconnect an IPCP component from a destination.
+ """
+ Disconnect an IPCP component from a destination.
:param pid: PID of the IPCP.
- :param component: Component to disconnect.
+ :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,
@@ -427,17 +496,24 @@ def bind_program(prog: str,
name: str,
opts: int = 0,
argv: Optional[List[str]] = None) -> None:
- """Bind a program to a name.
+ """
+ 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 to pass when the program is started.
+ :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)
- _argv = ffi.new("char *[]", [ffi.new("char[]", a.encode())
- for a in argv])
+ _args = []
+
+ for a in argv:
+ _args.append(ffi.new("char[]", a.encode()))
+
+ _argv = ffi.new("char *[]", _args)
else:
argc = 0
_argv = ffi.NULL
@@ -450,64 +526,85 @@ def bind_program(prog: str,
def unbind_program(prog: str, name: str) -> None:
- """Unbind a program from a name.
+ """
+ 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.
+ """
+ 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.
+ """
+ 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.
+ """
+ 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.
+ """
+ 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.
+ """
+ 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),
@@ -530,20 +627,27 @@ def list_names() -> List[NameInfo]:
def reg_name(name: str, pid: int) -> None:
- """Register an IPCP to a name.
+ """
+ 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.
+ """
+ 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 f1b6d11..c42a62e 100644
--- a/ouroboros/qos.py
+++ b/ouroboros/qos.py
@@ -1,25 +1,11 @@
#
-# Ouroboros - Copyright (C) 2016 - 2026
-#
-# Python API for Ouroboros - 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
#
-"""QoS specification for Ouroboros flows."""
+"""
+QoS specification for Ouroboros flows.
+"""
from __future__ import annotations
@@ -32,7 +18,10 @@ UINT64_MAX = 0xFFFFFFFFFFFFFFFF
class QoSService(IntEnum):
- """Mirrors ``enum qos_service`` in ``ouroboros/qos.h``."""
+ """
+ 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
@@ -40,11 +29,8 @@ class QoSService(IntEnum):
@dataclass(frozen=True)
class QoSSpec:
- """QoS specification for a flow.
-
- Frozen so the module-level ``QOS_*`` predefined cubes are safe to
- share. Construct a new spec with ``dataclasses.replace(qos, ...)``
- or ``QoSSpec(...)`` to override fields.
+ """
+ QoS specification for a flow.
:ivar service: :class:`QoSService` (gates FRCT when > 0).
:ivar delay: Maximum one-way delay in ms.
@@ -55,6 +41,7 @@ class QoSSpec:
:ivar max_gap: Maximum interruption in ms.
:ivar timeout: Peer timeout in ms (default 120000 ms).
"""
+
service: QoSService = QoSService.RAW
delay: int = UINT32_MAX
bandwidth: int = 0