aboutsummaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md360
1 files changed, 255 insertions, 105 deletions
diff --git a/README.md b/README.md
index 0d57a5e..5871d8c 100644
--- a/README.md
+++ b/README.md
@@ -4,51 +4,73 @@
## Dependencies
pyOuroboros requires <a href="https://ouroboros.rocks">Ouroboros</a>
-to be installed
+to be installed (matching `major.minor` version — see [Versioning](#versioning)).
## Installation
-To build and install PyOuroboros:
+
+To build and install pyOuroboros:
+
+```shell
+pip install .
+```
+
+Or for an editable install during development:
```shell
-./setup.py install
+pip install -e .
```
## Basic Usage
+Applications import each part of the API from its submodule
+explicitly — flows, events and the control plane are three distinct
+concerns, and the import line says which one is in use:
+
```Python
-from ouroboros.dev import *
+from ouroboros.dev import Flow, flow_alloc, flow_accept, flow_join
+from ouroboros.event import FEventQueue, FEventType, FlowSet
+from ouroboros.irm import create_ipcp, bootstrap_ipcp, reg_name
+# or, for IRM operations that mirror the `irm` CLI tool:
+from ouroboros.cli import autoboot, destroy_ipcp
```
-Server side: Accepting a flow:
+The pure-Python `errors` and `qos` modules are also re-exported at
+the top level for convenience (`from ouroboros import QoSSpec,
+OuroborosError`), but `dev`, `event`, `irm` and `cli` are only
+available through their submodule path — importing them eagerly
+from the top-level package would load the libouroboros CFFI
+extensions on every `import ouroboros`, which is the wrong default
+for a binding layer.
+
+Server side: accept a flow.
```Python
f = flow_accept()
```
-returns a new allocated flow object.
+returns a new allocated `Flow` object.
-Client side: Allocating a flow to a certain _name_:
+Client side: allocate a flow to a certain _name_.
```Python
f = flow_alloc("name")
```
-returns a new allocated Flow object.
-
-Broadcast:
+Broadcast: join a broadcast layer.
```Python
f = flow_join("name")
```
-returns a new allocated Flow object.
-
-Deallocation:
+Deallocate a flow:
```Python
f.dealloc()
```
-To avoid having to call dealloc(), you can use the with statement:
+
+`dealloc()` is idempotent. To avoid calling it explicitly, use a
+`with` statement — and any `Flow` that is garbage-collected without
+being deallocated will be cleaned up too:
```Python
with flow_alloc("dst") as f:
@@ -56,71 +78,114 @@ with flow_alloc("dst") as f:
print(f.readline())
```
-deallocates the flow. After this call, the Flow object is not readable
-or writeable anymore.
+`Flow()` constructs an empty wrapper; `Flow(fd)` wraps an existing
+flow descriptor. `f.alloc("name")` populates an empty wrapper:
```Python
+f = Flow()
f.alloc("name")
```
- will allocate a new flow for an existing Flow object.
-
-To read / write from a flow:
+To read / write on a flow:
```Python
-f.read(count) # read up to _count_ bytes and return bytes
-f.readline(count) # read up to _count_ characters as a string
-f.write(buf, count) # write up to _count_ bytes from buffer
-f.writeline(str, count) # write up to _count_ characters from string
+f.read(count) # read up to `count` bytes, return bytes
+f.readline() # read and decode as UTF-8, return str
+f.write(buf, count) # write up to `count` bytes from buffer
+f.writeline(ln) # encode `ln` as UTF-8 and write, return bytes written
```
## Quality of Service (QoS)
-The QoS spec details have not been finalized in Ouroboros. It is just
-here to give a general idea and to control some basics of the flow.
-You can specify a QoSSpec for flow allocation.
-
-For instance,
+`QoSSpec` describes the QoS requested for a flow. It is a **frozen**
+dataclass; construct a new spec to vary fields, or use
+`dataclasses.replace(qos, ...)` to derive one.
```Python
-qos = QoSSpec(loss=0, timeout=60000)
+from ouroboros.qos import QoSSpec, QoSService
+
+qos = QoSSpec(service=QoSService.MESSAGE, loss=0, timeout=60000)
f = flow_alloc("name", qos)
```
-will create a new flow with FRCP retransmission enabled that will
-timeout if the peer is not responsive for 1 minute.
+The `service` field selects the framing / reliability class and
+enables FRCT for values > 0:
+
+```Python
+class QoSService(IntEnum):
+ RAW # No FRCT; best-effort raw messages
+ MESSAGE # FRCT, reliable ordered messages
+ STREAM # FRCT, reliable ordered byte stream
+```
+
+A handful of predefined QoS specs mirror the ones in
+`ouroboros/qos.h` (`_safe` enables an integrity check by setting
+`ber=0`; `rt` trades reliability for latency):
+
+```Python
+from ouroboros import (
+ QOS_RAW, QOS_RAW_SAFE, # Raw best-effort
+ QOS_RT, QOS_RT_SAFE, # Real-time, low latency
+ QOS_MSG, # Reliable ordered messages
+ QOS_STREAM, # Reliable ordered byte stream
+)
+
+f = flow_alloc("name", QOS_STREAM)
+```
## Manipulating flows
-A number of methods are available for how to interact with Flow
+A number of methods are available to inspect and tune a `Flow`:
```Python
-f.set_snd_timeout(0.5) # set timeout for blocking write
-f.set_rcv_timeout(1.0) # set timeout for blocking read
+f.set_snd_timeout(0.5) # set timeout for blocking write (seconds)
+f.set_rcv_timeout(1.0) # set timeout for blocking read (seconds)
f.get_snd_timeout() # get timeout for blocking write
f.get_rcv_timeout() # get timeout for blocking read
f.get_qos() # get the QoSSpec for this flow
-f.get_rx_queue_len() # get the number of packets in the rx buffer
-f.get_tx_queue_len() # get the number of packets in the tx buffer
-f.set_flags(flags) # set a number of flags for this flow
+f.get_rx_queue_len() # bytes pending in the rx buffer
+f.get_tx_queue_len() # bytes pending in the tx buffer
+f.get_mtu() # per-packet MTU (0 if unknown)
+f.set_flags(flags) # replace the full set of flags
+f.add_flags(flags) # OR new flags into the current flow flags
+f.remove_flags(flags) # clear flags while preserving the rest
f.get_flags() # get the flags for this flow
+f.fileno() # underlying ouroboros flow descriptor
```
-The flags are specified as an enum FlowProperties:
+The flags are specified as an `IntFlag` enum, `FlowProperties`:
```Python
class FlowProperties(IntFlag):
- ReadOnly
- WriteOnly
- ReadWrite
- Down
- NonBlockingRead
- NonBlockingWrite
- NonBlocking
- NoPartialRead
- NoPartialWrite
+ READ_ONLY
+ WRITE_ONLY
+ READ_WRITE
+ DOWN
+ NON_BLOCKING_READ
+ NON_BLOCKING_WRITE
+ NON_BLOCKING # NON_BLOCKING_READ | NON_BLOCKING_WRITE
+ NO_PARTIAL_READ
+ NO_PARTIAL_WRITE
```
+For FRCT-enabled flows (`service > 0`), the FRCT state can be tuned:
+
+```Python
+from ouroboros.dev import FrctFlags
+
+f.set_frct_flags(FrctFlags.RESCNTL | FrctFlags.LINGER)
+f.get_frct_flags()
+f.set_frct_max_sdu(size) # max reassembly SDU (bytes)
+f.get_frct_max_sdu()
+f.set_frct_rcv_ring_size(size) # stream rcv ring (bytes, pow2)
+f.get_frct_rcv_ring_size()
+f.set_frct_rto_min(rto_ns) # RTO floor in nanoseconds
+f.get_frct_rto_min()
+```
+
+FRCT flags: `FrctFlags.RETRANSMIT` (fixed at flow alloc),
+`FrctFlags.RESCNTL`, `FrctFlags.LINGER`.
+
See the Ouroboros fccntl documentation for more details.
```shell
@@ -129,76 +194,131 @@ man fccntl
## Event API
-Multiple flows can be monitored for activity in parallel using a
-FlowSet and FEventQueue objects.
+Multiple flows can be monitored for activity in parallel using
+`FlowSet` and `FEventQueue` objects.
-FlowSets allow grouping a bunch of Flow objects together to listen for
-activity. It can be constructed with an optional list of Flows, or
-flows can be added or removed using the following methods:
+A `FlowSet` groups `Flow` objects together. It can be constructed
+with an optional list of flows; flows can be added or removed at any
+time:
```Python
-set = FlowSet() # create a flow set,
-set.add(f) # add a Flow 'f' to this set
-set.remove(f) # remove a Flow 'f' from this set
-set.zero() # remove all Flows in this set
+from ouroboros.event import FlowSet
+
+fs = FlowSet() # create an empty flow set
+fs.add(f) # add a Flow `f` to this set
+fs.remove(f) # remove a Flow `f` from this set
+fs.zero() # remove all Flows from this set
```
-An FEventQueue stores pending events on flows.
+An `FEventQueue` stores pending events on flows. Event types:
-The event types are defined as follows:
```Python
class FEventType(IntFlag):
- FlowPkt
- FlowDown
- FlowUp
- FlowAlloc
- FlowDealloc
- FlowPeer
+ FLOW_PKT
+ FLOW_DOWN
+ FLOW_UP
+ FLOW_ALLOC
+ FLOW_DEALLOC
+ FLOW_PEER
```
-and can be obtained by calling the next method:
+`FlowSet.wait()` populates an `FEventQueue` from a set; pending
+events are then drained via `FEventQueue.next()`:
```Python
- f, t = fq.next() # Return active flow 'f' and type of event 't'
-```
+from ouroboros.event import FEventQueue, FEventType, FlowSet
-An FEventQueue is populated from a FlowSet.
-
-```Python
-fq = FEventQueue() # Create an eventqueue
-set = FlowSet([f1, f2, f3]) # Create a new set with a couple of Flow objects
-set.wait(fq, timeo=1.0) # Wait for 1 second or until event
-while f, t = fq.next():
- if t == FEventType.FlowPkt:
+fq = FEventQueue()
+fs = FlowSet([f1, f2, f3])
+fs.wait(fq, timeo=1.0) # block up to 1 second
+while True:
+ try:
+ f, t = fq.next()
+ except OuroborosError:
+ break # queue drained
+ if t == FEventType.FLOW_PKT:
msg = f.readline()
...
-set.destroy()
+fs.destroy()
```
-A flow_set must be destroyed when it goes out of scope.
-To avoid having to call destroy, Python's with statement can be used:
+Both `FlowSet` and `FEventQueue` are context managers (preferred):
```Python
-fq = FEventQueue()
-with FlowSet([f]) as fs:
+with FEventQueue() as fq, FlowSet([f]) as fs:
fs.wait(fq)
-f2, t = fq.next()
-if t == FEventType.FlowPkt:
- line = f2.readline()
+ f2, t = fq.next()
+ if t == FEventType.FLOW_PKT:
+ line = f2.readline()
+```
+
+`destroy()` is idempotent; GC will also reclaim the underlying C
+handles when the wrapper goes out of scope.
+
+## Error handling
+
+All pyOuroboros exceptions derive from `OuroborosError`. The
+hierarchy mirrors stdlib conventions: flow-plane and IRM-plane errors
+have their own base classes, and several specific subclasses inherit
+from Python builtins so they can be caught either way.
+
+```
+OuroborosError
+├── IrmError
+│ ├── IpcpCreateError, IpcpBootstrapError, IpcpEnrollError
+│ ├── IpcpConnectError, IpcpTypeError (ValueError), IpcpStateError
+│ ├── IrmdError, IpcpdError, BindError
+│ └── NameNotFoundError, NameExistsError, InvalidNameError (ValueError)
+└── FlowError
+ ├── FlowAlreadyAllocatedError, FlowNotAllocatedError
+ ├── FlowDownError (ConnectionError), FlowPeerError (FlowDownError)
+ ├── FlowPermissionError (PermissionError)
+ ├── FlowTimeout (TimeoutError)
+ ├── FlowCryptError, FlowAuthError, FlowReplayError
+ └── FlowEventError
+FlowDeallocWarning (Warning)
+```
+
+ouroboros errno codes (`ENOTALLOC`, `EFLOWDOWN`, `EFLOWPEER`,
+`ENAME`, `ECRYPT`, ...) translate to the matching semantic subclass;
+ambiguous libc errnos (`ETIMEDOUT`, `EAGAIN`, `ENOTCONN`,
+`ECONNRESET`) translate to the stdlib base class.
+
+```Python
+from ouroboros import OuroborosError, FlowDownError
+
+try:
+ f.read()
+except TimeoutError:
+ ... # rcv timeout elapsed
+except FlowDownError:
+ ... # peer went away
+except OuroborosError as e:
+ log.warning("flow error: %s", e)
```
## IRM API
-The IRM (IPC Resource Manager) module allows managing IPCPs, names,
-and bindings programmatically.
+The IRM (IPC Resource Manager) module exposes the raw C API for
+managing IPCPs, names, and bindings:
```Python
-from ouroboros.irm import *
+from ouroboros.irm import (
+ IpcpType, IpcpConfig, NameInfo, BIND_AUTO, DT_COMP, MGMT_COMP,
+ create_ipcp, bootstrap_ipcp, enroll_ipcp, destroy_ipcp, list_ipcps,
+ connect_ipcp, disconnect_ipcp,
+ create_name, destroy_name, list_names, reg_name, unreg_name,
+ bind_program, unbind_program, bind_process, unbind_process,
+)
```
+For most use cases, the higher-level [`ouroboros.cli`](#cli-helpers)
+wrappers are easier to work with — they mirror the `irm` CLI tool and
+take IPCP names rather than pids.
+
### IPCP Management
-Creating, bootstrapping, enrolling, and destroying IPCPs:
+Create, bootstrap, enroll, and destroy IPCPs:
```Python
# Create a local IPCP
@@ -224,24 +344,24 @@ IPCP types: `LOCAL`, `UNICAST`, `BROADCAST`, `ETH_LLC`, `ETH_DIX`,
### IPCP Configuration
-The `IpcpConfig` class is used to bootstrap an IPCP. It takes
-the following parameters:
+`IpcpConfig` is a dataclass used to bootstrap an IPCP. It takes
+the following fields:
```Python
IpcpConfig(
- ipcp_type, # IpcpType (required)
- layer_name="", # Layer name (string)
- dir_hash_algo=DirectoryHashAlgo.SHA3_256, # Hash algorithm
- unicast=None, eth=None, udp4=None, udp6=None # Type-specific config
+ ipcp_type, # IpcpType (required)
+ layer_name="", # Layer name (string)
+ dir_hash_algo=DirectoryHashAlgo.SHA3_256, # Hash algorithm
+ unicast=None, eth=None, udp4=None, udp6=None # Type-specific config
)
```
-The `dir_hash_algo` can be set to `SHA3_224`, `SHA3_256`, `SHA3_384`,
-or `SHA3_512`.
+`dir_hash_algo` can be `SHA3_224`, `SHA3_256`, `SHA3_384`, or
+`SHA3_512`.
#### Local and Broadcast IPCPs
-Local and Broadcast IPCPs need no type-specific configuration:
+Local and broadcast IPCPs need no type-specific configuration:
```Python
conf = IpcpConfig(ipcp_type=IpcpType.LOCAL, layer_name="local_layer")
@@ -250,8 +370,7 @@ conf = IpcpConfig(ipcp_type=IpcpType.BROADCAST, layer_name="bc_layer")
#### Unicast IPCPs
-Unicast IPCPs have the most detailed configuration, structured as
-follows:
+Unicast IPCPs have the most detailed configuration:
```Python
conf = IpcpConfig(
@@ -283,7 +402,7 @@ conf = IpcpConfig(
)
),
addr_auth=AddressAuthPolicy.FLAT_RANDOM,
- cong_avoid=CongestionAvoidPolicy.MB_ECN # or CA_NONE
+ cong_avoid=CongestionAvoidPolicy.MB_ECN # or NONE
)
)
```
@@ -344,23 +463,24 @@ conf = IpcpConfig(
### Connecting IPCP Components
-Connecting and disconnecting IPCP components:
-
```Python
-connect_ipcp(pid, DT_COMP, "destination")
+connect_ipcp(pid, DT_COMP, "destination") # data transfer plane
+connect_ipcp(pid, MGMT_COMP, "destination") # management plane
disconnect_ipcp(pid, DT_COMP, "destination")
```
+`connect_ipcp` optionally takes a `qos=QoSSpec(...)` argument.
+
### Name Management
-Creating, destroying, and listing names:
+Create, destroy, and list names:
```Python
# Create a name
info = NameInfo(name="my_name", pol_lb=LoadBalancePolicy.ROUND_ROBIN)
create_name(info)
-# Register/unregister an IPCP to a name
+# Register / unregister an IPCP to a name
reg_name("my_name", pid)
unreg_name("my_name", pid)
@@ -384,9 +504,38 @@ bind_process(pid, "my_name")
unbind_process(pid, "my_name")
```
+## CLI helpers
+
+`ouroboros.cli` mirrors the C-side `ouroboros/tools/irm` command-line
+tool. The functions take IPCP _names_ (not pids), call `realpath()`
+on programs, and handle the `autobind` flag for bootstrap / enroll:
+
+```Python
+from ouroboros.cli import (
+ create_ipcp, destroy_ipcp, bootstrap_ipcp, enroll_ipcp,
+ connect_ipcp, disconnect_ipcp,
+ bind_program, bind_ipcp, autoboot,
+ reg_name, unreg_name,
+ IpcpType, IpcpConfig, # re-exported for convenience
+)
+
+# Create + bootstrap + autobind in one step
+autoboot("my_ipcp", IpcpType.LOCAL, layer="my_layer")
+
+# Bootstrap with autobind (binds the IPCP to its name and layer first)
+conf = IpcpConfig(ipcp_type=IpcpType.UNICAST, layer_name="dc")
+bootstrap_ipcp("my_ipcp", conf, autobind=True)
+
+# Register a name with multiple IPCPs at once
+reg_name("server", ipcps=["ipcp1", "ipcp2"])
+```
+
+See the module docstring for the full CLI-command ↔ Python mapping.
+
## Examples
-Some example code is in the examples folder.
+See the [`examples/`](examples/) folder for runnable client/server
+demos.
## Versioning
@@ -400,4 +549,5 @@ pyOuroboros uses `setuptools_scm` to derive its version from git tags.
| pyouroboros ↔ rumba ↔ ouroboros-integration | Strict lockstep `major.minor.patch` — always released together |
## License
+
pyOuroboros is LGPLv2.1. The examples are 3-clause BSD.