Rename bindspace resource models
Replace the unshipped `BindspaceIdentity` and `BindspaceHandle` names with `BindspaceRef` and `Bindspace` across existing lifecycle APIs. Deats, - define refs as wire-safe, host-local and non-owning records - reserve `Bindspace` for the live FD-backed capability - rename the capability's realized-resource field to `.ref` - update lifecycle tests and active design contracts - omit compatibility aliases for the unshipped model names Prompt-IO: ai/prompt-io/opencode/20260827T211115Z_d130431c_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
parent
cdd5591428
commit
932722b325
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
model: gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
session: tractor-addr-unpacking
|
||||||
|
timestamp: 2026-08-27T21:11:15Z
|
||||||
|
git_ref: d130431c
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20260827T211115Z_d130431c_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
During review, the human requested concrete names for the existing
|
||||||
|
bindspace abstractions and asked to isolate that rename before realized
|
||||||
|
reference retention.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Renamed the serializable resource record to `BindspaceRef`, the live
|
||||||
|
FD-backed capability to `Bindspace`, and the capability's record field
|
||||||
|
to `.ref`. Updated existing lifecycle APIs, tests and active design
|
||||||
|
contracts without compatibility aliases.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tractor/discovery/_bindspace.py` - renamed models and `.ref` field.
|
||||||
|
- `tractor/discovery/_tunnel.py` - existing lifecycle annotations.
|
||||||
|
- `tractor/discovery/__init__.py` - renamed public model exports.
|
||||||
|
- `tests/discovery/test_bindspace.py` - renamed model contracts.
|
||||||
|
- `tests/discovery/test_wg_iface_lifecycle.py` - live bindspace usage.
|
||||||
|
- `ai/tpt-backends/00_shared_backend_contract.md` - shared terminology.
|
||||||
|
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - request, ref and live
|
||||||
|
capability terminology.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
The human identified the earlier identity/handle terminology as too
|
||||||
|
abstract for an IPC-passed non-owning reference and live resource. They
|
||||||
|
selected `BindspaceRef`, `Bindspace`, and `.ref`, then requested this
|
||||||
|
rename as a prerequisite commit separate from the realized-ref feature.
|
||||||
|
The agent applied those human-directed names; no direct manual source
|
||||||
|
edits were observed.
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
model: gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
timestamp: 2026-08-27T21:11:15Z
|
||||||
|
git_ref: d130431c
|
||||||
|
diff_cmd: git diff HEAD~1..HEAD
|
||||||
|
---
|
||||||
|
|
||||||
|
# Raw output - simplify bindspace model names
|
||||||
|
|
||||||
|
While reviewing realized bindspace metadata, the human requested more
|
||||||
|
concrete names for the existing bindspace model family and asked that
|
||||||
|
the rename remain separate from the subsequent feature.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tractor/discovery/_bindspace.py tractor/discovery/_tunnel.py tractor/discovery/__init__.py tests/discovery/test_bindspace.py tests/discovery/test_wg_iface_lifecycle.py ai/tpt-backends/00_shared_backend_contract.md ai/tpt-backends/03_wg_tunnel_bindspace.md`
|
||||||
|
|
||||||
|
Renamed `BindspaceIdentity` to `BindspaceRef` for the serializable,
|
||||||
|
non-owning, host-local resource record. Renamed `BindspaceHandle` to
|
||||||
|
`Bindspace` for the live process-local FD-backed capability, and renamed
|
||||||
|
its `.identity` field to `.ref`.
|
||||||
|
|
||||||
|
No compatibility aliases were added because the models are unshipped.
|
||||||
|
Active design contracts now distinguish the serialized request, the
|
||||||
|
wire-safe non-owning ref, and the live capability. Ruff passed, 518
|
||||||
|
tests collected, and 53 focused bindspace/WireGuard/listener tests
|
||||||
|
passed across the final two-commit tree.
|
||||||
|
|
@ -115,9 +115,10 @@ Hard constraints learned from the existing two:
|
||||||
*ALPN + relay/discovery realm* (plan 02). Do not overload this
|
*ALPN + relay/discovery realm* (plan 02). Do not overload this
|
||||||
transport-level bind selector with process namespace lifecycle.
|
transport-level bind selector with process namespace lifecycle.
|
||||||
Plan 03 augments an maddr/address declaration with a serializable
|
Plan 03 augments an maddr/address declaration with a serializable
|
||||||
`BindspaceSpec` and a scoped, non-serializable `BindspaceHandle`;
|
`BindspaceSpec` request and host-local `BindspaceRef`. A scoped,
|
||||||
the latter owns namespace identity/FD/lifetime and is consumed at
|
non-serializable `Bindspace` carries that ref and owns the FD/lifetime
|
||||||
spawn bootstrap before a concrete address reaches transport bind.
|
used during spawn bootstrap before a concrete address reaches
|
||||||
|
transport bind.
|
||||||
`Address.namespace` is already spec'd in the Protocol as
|
`Address.namespace` is already spec'd in the Protocol as
|
||||||
"the if-available OS-specific network namespace key" and is
|
"the if-available OS-specific network namespace key" and is
|
||||||
currently unimplemented by both backends — plan 03 is the
|
currently unimplemented by both backends — plan 03 is the
|
||||||
|
|
|
||||||
|
|
@ -378,7 +378,7 @@ Keep the authority surface deliberately small:
|
||||||
|
|
||||||
The root owns the manager's lifetime. `wgman` must outlive all
|
The root owns the manager's lifetime. `wgman` must outlive all
|
||||||
siblings borrowing its tunnels and exit before the root drops the
|
siblings borrowing its tunnels and exit before the root drops the
|
||||||
underlying namespace/capability handles. A manager crash fails closed:
|
underlying namespace capabilities. A manager crash fails closed:
|
||||||
dependent operations receive an explicit service error; restart, if
|
dependent operations receive an explicit service error; restart, if
|
||||||
enabled, reconciles declared state idempotently before advertising
|
enabled, reconciles declared state idempotently before advertising
|
||||||
readiness again. Do not silently let siblings fall back to privileged
|
readiness again. Do not silently let siblings fall back to privileged
|
||||||
|
|
@ -400,8 +400,8 @@ select the local instance of that network stack. A netns, VRF,
|
||||||
interface, user namespace, or equivalent platform resource is
|
interface, user namespace, or equivalent platform resource is
|
||||||
orthogonal augmentation carried alongside/below the maddr.
|
orthogonal augmentation carried alongside/below the maddr.
|
||||||
|
|
||||||
Keep two bindspace representations with deliberately different
|
Keep three bindspace representations with deliberately different roles
|
||||||
lifetimes:
|
and lifetimes:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class BindspaceSpec(msgspec.Struct, frozen=True):
|
class BindspaceSpec(msgspec.Struct, frozen=True):
|
||||||
|
|
@ -411,17 +411,17 @@ class BindspaceSpec(msgspec.Struct, frozen=True):
|
||||||
lifecycle: Literal['attach', 'open']
|
lifecycle: Literal['attach', 'open']
|
||||||
|
|
||||||
|
|
||||||
class BindspaceIdentity(msgspec.Struct, frozen=True):
|
class BindspaceRef(msgspec.Struct, frozen=True):
|
||||||
'''Stable identity of the realized platform resource.'''
|
'''Wire-safe, non-owning ref to the realized resource.'''
|
||||||
kind: str
|
kind: str
|
||||||
key: str|None # mutable name, absent after unlink
|
key: str|None # mutable name, absent after unlink
|
||||||
inode: int # stable Linux namespace identity
|
inode: int # host-local Linux nsfs fingerprint
|
||||||
|
|
||||||
|
|
||||||
class BindspaceHandle(ProcessLocal):
|
class Bindspace(ProcessLocal):
|
||||||
'''Scoped, non-serializable capability for one live bindspace.'''
|
'''Scoped, non-serializable capability for one live bindspace.'''
|
||||||
spec: BindspaceSpec
|
spec: BindspaceSpec
|
||||||
identity: BindspaceIdentity
|
ref: BindspaceRef
|
||||||
namespace_fd: int|None
|
namespace_fd: int|None
|
||||||
ownership: Literal['owned', 'borrowed']
|
ownership: Literal['owned', 'borrowed']
|
||||||
|
|
||||||
|
|
@ -429,7 +429,7 @@ class BindspaceHandle(ProcessLocal):
|
||||||
@acm
|
@acm
|
||||||
async def open_bindspace(
|
async def open_bindspace(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
) -> AsyncGenerator[BindspaceHandle, None]:
|
) -> AsyncGenerator[Bindspace, None]:
|
||||||
'''
|
'''
|
||||||
Provision/borrow one bindspace and yield its live capability.
|
Provision/borrow one bindspace and yield its live capability.
|
||||||
|
|
||||||
|
|
@ -437,15 +437,17 @@ async def open_bindspace(
|
||||||
```
|
```
|
||||||
|
|
||||||
The initial model limits `BindspaceKind` to `netns` while preserving
|
The initial model limits `BindspaceKind` to `netns` while preserving
|
||||||
the required lifetime split. `BindspaceSpec` and
|
the required role split. `BindspaceSpec` is the requested resource and
|
||||||
`BindspaceIdentity` are frozen msgspec structs which cross
|
lifecycle policy. `BindspaceRef` is a serializable, non-owning,
|
||||||
config/spawn serialization. `BindspaceHandle` also uses msgspec's
|
host-local record of the resource that was actually opened; it can be
|
||||||
generic struct storage by inheriting the global
|
compared or logged, but cannot reopen, pin or enter that resource.
|
||||||
`tractor.msg.ProcessLocal` marker. Its hidden unsupported sentinel
|
`Bindspace` is the live capability and uses msgspec's generic struct
|
||||||
blocks direct and nested default msgspec encoding without a recursive
|
storage by inheriting the global `tractor.msg.ProcessLocal` marker. Its
|
||||||
IPC hot-path scan. The handle validates any supplied FD against
|
hidden unsupported sentinel blocks direct and nested default msgspec
|
||||||
`BindspaceIdentity.inode`; explicit FD transfer belongs to the
|
encoding without a recursive IPC hot-path scan. The live bindspace
|
||||||
supervisor bootstrap path. An FD avoids name-resolution TOCTOU,
|
validates any supplied FD against `BindspaceRef.inode`; explicit FD
|
||||||
|
transfer belongs to the supervisor bootstrap path. An FD avoids
|
||||||
|
name-resolution TOCTOU,
|
||||||
survives rename/unlink, and identifies the exact namespace the parent
|
survives rename/unlink, and identifies the exact namespace the parent
|
||||||
provisioned. Extend the kind/field union only when a second platform
|
provisioned. Extend the kind/field union only when a second platform
|
||||||
resource is implemented.
|
resource is implemented.
|
||||||
|
|
@ -459,7 +461,7 @@ or locally owned networking.
|
||||||
The first lifecycle implementation is deliberately borrow-only:
|
The first lifecycle implementation is deliberately borrow-only:
|
||||||
`attach_netns()` opens either `/proc/self/ns/net` when
|
`attach_netns()` opens either `/proc/self/ns/net` when
|
||||||
`BindspaceSpec.key = CURRENT_NETNS`, or a named entry beneath
|
`BindspaceSpec.key = CURRENT_NETNS`, or a named entry beneath
|
||||||
`/var/run/netns`. It derives identity from the opened FD, yields
|
`/var/run/netns`. It derives a `BindspaceRef` from the opened FD, yields
|
||||||
`ownership='borrowed'`, and closes only that FD on exit. "Attach" does
|
`ownership='borrowed'`, and closes only that FD on exit. "Attach" does
|
||||||
not call `setns()`; it never creates, enters or removes a namespace.
|
not call `setns()`; it never creates, enters or removes a namespace.
|
||||||
Future `open_netns()` creation and owned teardown remain a separate
|
Future `open_netns()` creation and owned teardown remain a separate
|
||||||
|
|
@ -474,7 +476,8 @@ spawn/bootstrap operation.
|
||||||
|
|
||||||
`open_bindspace()` is **not** an address factory and does not return a
|
`open_bindspace()` is **not** an address factory and does not return a
|
||||||
`TunnelledAddress`. At the declaration layer, listener allocation can
|
`TunnelledAddress`. At the declaration layer, listener allocation can
|
||||||
use the handle to replace an overlay while preserving every tunnel:
|
use the live bindspace to replace an overlay while preserving every
|
||||||
|
tunnel:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async with open_bindspace(
|
async with open_bindspace(
|
||||||
|
|
@ -503,13 +506,13 @@ tunnel/bindspace layer:
|
||||||
@acm
|
@acm
|
||||||
async def open_netns(
|
async def open_netns(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
) -> AsyncGenerator[BindspaceHandle, None]: ...
|
) -> AsyncGenerator[Bindspace, None]: ...
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def open_wg_iface(
|
async def open_wg_iface(
|
||||||
spec: WGTunnelSpec,
|
spec: WGTunnelSpec,
|
||||||
config: WGInterfaceConfig,
|
config: WGInterfaceConfig,
|
||||||
bindspace: BindspaceHandle,
|
bindspace: Bindspace,
|
||||||
role: Literal['listen', 'dial'],
|
role: Literal['listen', 'dial'],
|
||||||
) -> AsyncGenerator[WGTunnelSpec, None]: ...
|
) -> AsyncGenerator[WGTunnelSpec, None]: ...
|
||||||
```
|
```
|
||||||
|
|
@ -542,7 +545,7 @@ entries. Extend it to carry the tunnel stack, not to *enter* it.
|
||||||
`open_wg_bindspace()` is the initial driver for one bindspace and an
|
`open_wg_bindspace()` is the initial driver for one bindspace and an
|
||||||
ordered sequence of `(WGTunnelSpec, WGInterfaceConfig)` layers. It
|
ordered sequence of `(WGTunnelSpec, WGInterfaceConfig)` layers. It
|
||||||
opens the bindspace first, enters WG interfaces outermost-first through
|
opens the bindspace first, enters WG interfaces outermost-first through
|
||||||
`AsyncExitStack`, and yields the live `BindspaceHandle` for endpoint
|
`AsyncExitStack`, and yields the live `Bindspace` for endpoint
|
||||||
allocation. Exit is inside-out, so every interface is removed while the
|
allocation. Exit is inside-out, so every interface is removed while the
|
||||||
namespace FD remains pinned; only then can an owned namespace be
|
namespace FD remains pinned; only then can an owned namespace be
|
||||||
removed. Endpoint/channel lifetimes belong inside the yielded scope.
|
removed. Endpoint/channel lifetimes belong inside the yielded scope.
|
||||||
|
|
@ -566,10 +569,10 @@ composed maddr can name a server source or client destination (§5.4).
|
||||||
|
|
||||||
Use `github/ns_aware@e4688cad` as prototype evidence, not code to
|
Use `github/ns_aware@e4688cad` as prototype evidence, not code to
|
||||||
cherry-pick unchanged. Its `/proc/<pid>/ns/<type>` inode reader and
|
cherry-pick unchanged. Its `/proc/<pid>/ns/<type>` inode reader and
|
||||||
`ip netns identify` probe establish the useful `(key, inode)` identity
|
`ip netns identify` probe establish the useful `(key, inode)` reference
|
||||||
pair. Layer C should move that shape into `BindspaceIdentity`, avoid a
|
record. Layer C should move that shape into `BindspaceRef`, avoid a
|
||||||
subprocess where netlink/procfs suffices, and hold the namespace FD in
|
subprocess where netlink/procfs suffices, and hold the namespace FD in
|
||||||
`BindspaceHandle` to pin the identity.
|
`Bindspace` to pin the referenced resource.
|
||||||
|
|
||||||
### 5.4 the netns/process reality — read this before designing
|
### 5.4 the netns/process reality — read this before designing
|
||||||
|
|
||||||
|
|
@ -612,7 +615,7 @@ server bound in the old namespace.
|
||||||
- a root/single-actor process follows the same ordering: enter during
|
- a root/single-actor process follows the same ordering: enter during
|
||||||
root bootstrap, never after actor runtime startup.
|
root bootstrap, never after actor runtime startup.
|
||||||
- iface/route/WG provisioning is genuinely scoped and remains under
|
- iface/route/WG provisioning is genuinely scoped and remains under
|
||||||
the parent/supervisor's `BindspaceHandle` context.
|
the parent/supervisor's `Bindspace` context.
|
||||||
- document the constraint rather than hiding it; a
|
- document the constraint rather than hiding it; a
|
||||||
`RuntimeError` if namespace entry is attempted after bootstrap.
|
`RuntimeError` if namespace entry is attempted after bootstrap.
|
||||||
- capabilities: iface/netns creation/config needs `CAP_NET_ADMIN`;
|
- capabilities: iface/netns creation/config needs `CAP_NET_ADMIN`;
|
||||||
|
|
@ -641,7 +644,7 @@ server bound in the old namespace.
|
||||||
- teardown follows capability ownership, not just address type:
|
- teardown follows capability ownership, not just address type:
|
||||||
- owned listener bindspaces tear down after endpoints/channels and
|
- owned listener bindspaces tear down after endpoints/channels and
|
||||||
the actor process have exited;
|
the actor process have exited;
|
||||||
- borrowed dial/actor-wide bindspaces only release their handle;
|
- borrowed dial/actor-wide bindspaces only release their capability;
|
||||||
- nested resources exit inside-out, but shared resources remain until
|
- nested resources exit inside-out, but shared resources remain until
|
||||||
their owning supervisor drops the final capability.
|
their owning supervisor drops the final capability.
|
||||||
- teardown must be idempotent and tolerant: an iface/netns
|
- teardown must be idempotent and tolerant: an iface/netns
|
||||||
|
|
@ -713,7 +716,7 @@ consider doing it *first* for exactly that reason.
|
||||||
| namespace name is renamed/replaced between provision and spawn | pass an open namespace FD; verify `(key, inode)` after child entry |
|
| namespace name is renamed/replaced between provision and spawn | pass an open namespace FD; verify `(key, inode)` after child entry |
|
||||||
| child starts sockets/threads before `setns()` | enter in the spawn bootstrap trampoline before `_runtime.async_main()`; assert inode ordering |
|
| child starts sockets/threads before `setns()` | enter in the spawn bootstrap trampoline before `_runtime.async_main()`; assert inode ordering |
|
||||||
| ambient capabilities leak into actor app code | split provision/enter authority and drop caps before runtime initialization |
|
| ambient capabilities leak into actor app code | split provision/enter authority and drop caps before runtime initialization |
|
||||||
| dial path tears down a shared actor bindspace | encode ownership in `BindspaceHandle`; borrowed handles never remove resources |
|
| dial path tears down a shared actor bindspace | encode ownership in `Bindspace`; borrowed bindspaces never remove resources |
|
||||||
| py-multiaddr#108 merged but unreleased | PEP 621 direct-revision pin + `_wg_proto_code()` gate; replace with a release floor once published |
|
| py-multiaddr#108 merged but unreleased | PEP 621 direct-revision pin + `_wg_proto_code()` gate; replace with a release floor once published |
|
||||||
| `TunnelledAddress` leaks into transport reflection/type dispatch | keep wrappers through declaration/bindspace handling, call `strip_tunnels()` at channel/endpoint boundaries, and retain the boundary regressions |
|
| `TunnelledAddress` leaks into transport reflection/type dispatch | keep wrappers through declaration/bindspace handling, call `strip_tunnels()` at channel/endpoint boundaries, and retain the boundary regressions |
|
||||||
| privileged ops in a library | never `sudo`; explicit cap probe + actionable error; pre-provisioned is the default |
|
| privileged ops in a library | never `sudo`; explicit cap probe + actionable error; pre-provisioned is the default |
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
'''
|
'''
|
||||||
Bindspace declaration, identity and live-capability contracts.
|
Bindspace declaration, reference and live-capability contracts.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -14,9 +14,9 @@ import pytest
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from tractor.discovery import (
|
from tractor.discovery import (
|
||||||
BindspaceHandle,
|
Bindspace,
|
||||||
BindspaceIdentity,
|
|
||||||
BindspaceOwnership,
|
BindspaceOwnership,
|
||||||
|
BindspaceRef,
|
||||||
BindspaceSpec,
|
BindspaceSpec,
|
||||||
CURRENT_NETNS,
|
CURRENT_NETNS,
|
||||||
attach_netns,
|
attach_netns,
|
||||||
|
|
@ -29,15 +29,15 @@ from tractor.msg import ProcessLocal
|
||||||
|
|
||||||
def test_bindspace_declarations_roundtrip() -> None:
|
def test_bindspace_declarations_roundtrip() -> None:
|
||||||
'''
|
'''
|
||||||
Spawn configuration and realized identity must cross actor IPC.
|
Spawn configuration and realized refs must cross actor IPC.
|
||||||
|
|
||||||
Encode both frozen structs through msgpack and decode with their
|
Encode both frozen structs through msgpack and decode with their
|
||||||
concrete types, proving names and stable inode identity survive
|
concrete types, proving names and stable inode refs survive
|
||||||
without carrying any process-local capability state.
|
without carrying any process-local capability state.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
values: tuple[
|
values: tuple[
|
||||||
BindspaceSpec|BindspaceIdentity,
|
BindspaceSpec|BindspaceRef,
|
||||||
...,
|
...,
|
||||||
] = (
|
] = (
|
||||||
BindspaceSpec(
|
BindspaceSpec(
|
||||||
|
|
@ -45,16 +45,16 @@ def test_bindspace_declarations_roundtrip() -> None:
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
lifecycle='open',
|
lifecycle='open',
|
||||||
),
|
),
|
||||||
BindspaceIdentity(
|
BindspaceRef(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
inode=1234,
|
inode=1234,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
value: BindspaceSpec|BindspaceIdentity
|
value: BindspaceSpec|BindspaceRef
|
||||||
for value in values:
|
for value in values:
|
||||||
encoded: bytes = msgspec.msgpack.encode(value)
|
encoded: bytes = msgspec.msgpack.encode(value)
|
||||||
decoded: BindspaceSpec|BindspaceIdentity = (
|
decoded: BindspaceSpec|BindspaceRef = (
|
||||||
msgspec.msgpack.decode(
|
msgspec.msgpack.decode(
|
||||||
encoded,
|
encoded,
|
||||||
type=type(value),
|
type=type(value),
|
||||||
|
|
@ -63,16 +63,17 @@ def test_bindspace_declarations_roundtrip() -> None:
|
||||||
assert decoded == value
|
assert decoded == value
|
||||||
|
|
||||||
|
|
||||||
def test_bindspace_handle_pins_local_capability(
|
def test_bindspace_pins_local_capability(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
A live handle pins one exact FD and realized identity.
|
A live bindspace pins one exact FD and realized ref.
|
||||||
|
|
||||||
Open a stand-in platform handle, record its inode in the realized
|
Open a stand-in platform FD, record its inode in the realized
|
||||||
identity and construct an owned capability. Prove the generic
|
ref and construct an owned capability. Prove the generic
|
||||||
msgspec struct retains that exact local state. Its ability to
|
msgspec struct retains that exact local state. Its ability to
|
||||||
encode ordinary fields is not authority to transfer the handle.
|
encode ordinary fields is not authority to transfer the
|
||||||
|
bindspace.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
token_path: Path = tmp_path / 'bindspace'
|
token_path: Path = tmp_path / 'bindspace'
|
||||||
|
|
@ -86,36 +87,36 @@ def test_bindspace_handle_pins_local_capability(
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
lifecycle='open',
|
lifecycle='open',
|
||||||
)
|
)
|
||||||
identity: BindspaceIdentity = BindspaceIdentity(
|
ref: BindspaceRef = BindspaceRef(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
inode=inode,
|
inode=inode,
|
||||||
)
|
)
|
||||||
handle: BindspaceHandle = BindspaceHandle(
|
bindspace: Bindspace = Bindspace(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
identity=identity,
|
ref=ref,
|
||||||
namespace_fd=namespace_fd,
|
namespace_fd=namespace_fd,
|
||||||
ownership='owned',
|
ownership='owned',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert handle.spec is spec
|
assert bindspace.spec is spec
|
||||||
assert handle.identity is identity
|
assert bindspace.ref is ref
|
||||||
assert handle.namespace_fd == namespace_file.fileno()
|
assert bindspace.namespace_fd == namespace_file.fileno()
|
||||||
assert handle.ownership == 'owned'
|
assert bindspace.ownership == 'owned'
|
||||||
assert isinstance(handle, msgspec.Struct)
|
assert isinstance(bindspace, msgspec.Struct)
|
||||||
assert isinstance(handle, ProcessLocal)
|
assert isinstance(bindspace, ProcessLocal)
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
TypeError,
|
TypeError,
|
||||||
match='_ProcessLocalToken.*unsupported',
|
match='_ProcessLocalToken.*unsupported',
|
||||||
):
|
):
|
||||||
msgspec.msgpack.encode(handle)
|
msgspec.msgpack.encode(bindspace)
|
||||||
|
|
||||||
|
|
||||||
def test_bindspace_handle_rejects_mismatched_identity(
|
def test_bindspace_rejects_mismatched_ref(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
A name or inode mismatch would make a handle stale authority.
|
A name or inode mismatch would make a bindspace stale authority.
|
||||||
|
|
||||||
Construct a requested named spec, then prove both a different
|
Construct a requested named spec, then prove both a different
|
||||||
realized name and an inode not belonging to the supplied FD are
|
realized name and an inode not belonging to the supplied FD are
|
||||||
|
|
@ -128,39 +129,39 @@ def test_bindspace_handle_rejects_mismatched_identity(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
)
|
)
|
||||||
# Keep ownership and FD fixed so only identity changes below.
|
# Keep ownership and FD fixed so only the ref changes below.
|
||||||
ownership: BindspaceOwnership = 'borrowed'
|
ownership: BindspaceOwnership = 'borrowed'
|
||||||
namespace_file: BinaryIO
|
namespace_file: BinaryIO
|
||||||
with token_path.open('rb') as namespace_file:
|
with token_path.open('rb') as namespace_file:
|
||||||
namespace_fd: int = namespace_file.fileno()
|
namespace_fd: int = namespace_file.fileno()
|
||||||
wrong_name: BindspaceIdentity = BindspaceIdentity(
|
wrong_name: BindspaceRef = BindspaceRef(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='other-wg',
|
key='other-wg',
|
||||||
inode=token_path.stat().st_ino,
|
inode=token_path.stat().st_ino,
|
||||||
)
|
)
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ValueError,
|
ValueError,
|
||||||
match='Spec.key.*Identity.key',
|
match='Spec.key.*Ref.key',
|
||||||
):
|
):
|
||||||
BindspaceHandle(
|
Bindspace(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
identity=wrong_name,
|
ref=wrong_name,
|
||||||
namespace_fd=namespace_fd,
|
namespace_fd=namespace_fd,
|
||||||
ownership=ownership,
|
ownership=ownership,
|
||||||
)
|
)
|
||||||
|
|
||||||
wrong_inode: BindspaceIdentity = BindspaceIdentity(
|
wrong_inode: BindspaceRef = BindspaceRef(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
inode=token_path.stat().st_ino + 1,
|
inode=token_path.stat().st_ino + 1,
|
||||||
)
|
)
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ValueError,
|
ValueError,
|
||||||
match='FD inode.*identity inode',
|
match='FD inode.*reference inode',
|
||||||
):
|
):
|
||||||
BindspaceHandle(
|
Bindspace(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
identity=wrong_inode,
|
ref=wrong_inode,
|
||||||
namespace_fd=namespace_fd,
|
namespace_fd=namespace_fd,
|
||||||
ownership=ownership,
|
ownership=ownership,
|
||||||
)
|
)
|
||||||
|
|
@ -170,14 +171,14 @@ def test_bindspace_handle_rejects_mismatched_identity(
|
||||||
('model', 'kwargs', 'match'),
|
('model', 'kwargs', 'match'),
|
||||||
(
|
(
|
||||||
pytest.param(
|
pytest.param(
|
||||||
BindspaceIdentity,
|
BindspaceRef,
|
||||||
{
|
{
|
||||||
'kind': 'netns',
|
'kind': 'netns',
|
||||||
'key': None,
|
'key': None,
|
||||||
'inode': None,
|
'inode': None,
|
||||||
},
|
},
|
||||||
'must be a positive `int`',
|
'must be a positive `int`',
|
||||||
id='identity-requires-inode',
|
id='ref-requires-inode',
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
BindspaceSpec,
|
BindspaceSpec,
|
||||||
|
|
@ -186,14 +187,14 @@ def test_bindspace_handle_rejects_mismatched_identity(
|
||||||
id='spec-rejects-kind',
|
id='spec-rejects-kind',
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
BindspaceIdentity,
|
BindspaceRef,
|
||||||
{
|
{
|
||||||
'kind': 'vrf',
|
'kind': 'vrf',
|
||||||
'key': 'blue',
|
'key': 'blue',
|
||||||
'inode': 1234,
|
'inode': 1234,
|
||||||
},
|
},
|
||||||
'Unsupported bindspace kind',
|
'Unsupported bindspace kind',
|
||||||
id='identity-rejects-kind',
|
id='ref-rejects-kind',
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
BindspaceSpec,
|
BindspaceSpec,
|
||||||
|
|
@ -224,19 +225,19 @@ def test_bindspace_handle_rejects_mismatched_identity(
|
||||||
id='spec-rejects-lifecycle',
|
id='spec-rejects-lifecycle',
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
BindspaceIdentity,
|
BindspaceRef,
|
||||||
{
|
{
|
||||||
'kind': 'netns',
|
'kind': 'netns',
|
||||||
'key': '',
|
'key': '',
|
||||||
'inode': 1234,
|
'inode': 1234,
|
||||||
},
|
},
|
||||||
'BindspaceIdentity.key',
|
'BindspaceRef.key',
|
||||||
id='identity-rejects-empty-key',
|
id='ref-rejects-empty-key',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_bindspace_models_reject_invalid_values(
|
def test_bindspace_models_reject_invalid_values(
|
||||||
model: type[BindspaceSpec]|type[BindspaceIdentity],
|
model: type[BindspaceSpec]|type[BindspaceRef],
|
||||||
kwargs: dict[str, object],
|
kwargs: dict[str, object],
|
||||||
match: str,
|
match: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -245,7 +246,7 @@ def test_bindspace_models_reject_invalid_values(
|
||||||
|
|
||||||
Parameterize the missing stable inode and future, unimplemented
|
Parameterize the missing stable inode and future, unimplemented
|
||||||
kinds. Prove neither serializable model can carry invalid
|
kinds. Prove neither serializable model can carry invalid
|
||||||
identity or provisioning instructions into spawn configuration.
|
refs or provisioning instructions into spawn configuration.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
with pytest.raises(ValueError, match=match):
|
with pytest.raises(ValueError, match=match):
|
||||||
|
|
@ -260,7 +261,7 @@ def test_open_bindspace_attaches_current_netns() -> None:
|
||||||
'''
|
'''
|
||||||
The unnamed spec must borrow and pin the caller's current netns.
|
The unnamed spec must borrow and pin the caller's current netns.
|
||||||
|
|
||||||
Open `/proc/self/ns/net`, prove the yielded handle records its
|
Open `/proc/self/ns/net`, prove the yielded bindspace records its
|
||||||
stable inode and borrowed ownership, then exit the context and
|
stable inode and borrowed ownership, then exit the context and
|
||||||
prove the exact descriptor was closed without altering the
|
prove the exact descriptor was closed without altering the
|
||||||
namespace itself.
|
namespace itself.
|
||||||
|
|
@ -275,15 +276,15 @@ def test_open_bindspace_attaches_current_netns() -> None:
|
||||||
kind='netns',
|
kind='netns',
|
||||||
)
|
)
|
||||||
assert spec.key is CURRENT_NETNS
|
assert spec.key is CURRENT_NETNS
|
||||||
async with open_bindspace(spec) as handle:
|
async with open_bindspace(spec) as bindspace:
|
||||||
namespace_fd: int|None = handle.namespace_fd
|
namespace_fd: int|None = bindspace.namespace_fd
|
||||||
assert namespace_fd is not None
|
assert namespace_fd is not None
|
||||||
assert handle.spec is spec
|
assert bindspace.spec is spec
|
||||||
assert handle.identity.key is None
|
assert bindspace.ref.key is None
|
||||||
assert handle.identity.inode == os.fstat(
|
assert bindspace.ref.inode == os.fstat(
|
||||||
namespace_fd
|
namespace_fd
|
||||||
).st_ino
|
).st_ino
|
||||||
assert handle.ownership == 'borrowed'
|
assert bindspace.ownership == 'borrowed'
|
||||||
return namespace_fd
|
return namespace_fd
|
||||||
|
|
||||||
namespace_fd: int = trio.run(main)
|
namespace_fd: int = trio.run(main)
|
||||||
|
|
@ -324,12 +325,12 @@ def test_attach_named_netns_uses_run_directory(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
)
|
)
|
||||||
async with attach_netns(spec) as handle:
|
async with attach_netns(spec) as bindspace:
|
||||||
namespace_fd: int|None = handle.namespace_fd
|
namespace_fd: int|None = bindspace.namespace_fd
|
||||||
assert namespace_fd is not None
|
assert namespace_fd is not None
|
||||||
assert handle.identity.key == 'tractor-wg0'
|
assert bindspace.ref.key == 'tractor-wg0'
|
||||||
assert handle.identity.inode == netns_path.stat().st_ino
|
assert bindspace.ref.inode == netns_path.stat().st_ino
|
||||||
assert handle.ownership == 'borrowed'
|
assert bindspace.ownership == 'borrowed'
|
||||||
return namespace_fd
|
return namespace_fd
|
||||||
|
|
||||||
namespace_fd: int = trio.run(main)
|
namespace_fd: int = trio.run(main)
|
||||||
|
|
@ -389,7 +390,7 @@ def test_open_netns_owns_lifecycle(
|
||||||
Successful creation must yield ownership and remove on exit.
|
Successful creation must yield ownership and remove on exit.
|
||||||
|
|
||||||
Fake pyroute2 creation with a named stand-in file, verify the
|
Fake pyroute2 creation with a named stand-in file, verify the
|
||||||
yielded FD and identity while it exists, then prove FD closure
|
yielded FD and ref while it exists, then prove FD closure
|
||||||
precedes resource removal when the context exits.
|
precedes resource removal when the context exits.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
@ -444,11 +445,11 @@ def test_open_netns_owns_lifecycle(
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
lifecycle='open',
|
lifecycle='open',
|
||||||
)
|
)
|
||||||
async with open_bindspace(spec) as handle:
|
async with open_bindspace(spec) as bindspace:
|
||||||
fd: int|None = handle.namespace_fd
|
fd: int|None = bindspace.namespace_fd
|
||||||
assert fd is not None
|
assert fd is not None
|
||||||
assert handle.ownership == 'owned'
|
assert bindspace.ownership == 'owned'
|
||||||
assert handle.identity.inode == os.fstat(fd).st_ino
|
assert bindspace.ref.inode == os.fstat(fd).st_ino
|
||||||
namespace_fds.append(fd)
|
namespace_fds.append(fd)
|
||||||
events.append('yield')
|
events.append('yield')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ import pytest
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from tractor.discovery import (
|
from tractor.discovery import (
|
||||||
BindspaceHandle,
|
Bindspace,
|
||||||
BindspaceIdentity,
|
BindspaceRef,
|
||||||
BindspaceSpec,
|
BindspaceSpec,
|
||||||
WGInterfaceConfig,
|
WGInterfaceConfig,
|
||||||
WGPeerConfig,
|
WGPeerConfig,
|
||||||
|
|
@ -159,7 +159,7 @@ def test_open_wg_iface_shields_cancelled_cleanup(
|
||||||
def create(
|
def create(
|
||||||
spec: WGTunnelSpec,
|
spec: WGTunnelSpec,
|
||||||
config: WGInterfaceConfig,
|
config: WGInterfaceConfig,
|
||||||
bindspace: BindspaceHandle,
|
bindspace: Bindspace,
|
||||||
listen_port: int|None,
|
listen_port: int|None,
|
||||||
peers: tuple[dict[str, object], ...],
|
peers: tuple[dict[str, object], ...],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -174,7 +174,7 @@ def test_open_wg_iface_shields_cancelled_cleanup(
|
||||||
|
|
||||||
def remove(
|
def remove(
|
||||||
spec: WGTunnelSpec,
|
spec: WGTunnelSpec,
|
||||||
bindspace: BindspaceHandle,
|
bindspace: Bindspace,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Record shielded removal after cancellation.
|
Record shielded removal after cancellation.
|
||||||
|
|
@ -200,9 +200,9 @@ def test_open_wg_iface_shields_cancelled_cleanup(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
)
|
)
|
||||||
bindspace: BindspaceHandle = BindspaceHandle(
|
bindspace: Bindspace = Bindspace(
|
||||||
spec=bindspace_spec,
|
spec=bindspace_spec,
|
||||||
identity=BindspaceIdentity(
|
ref=BindspaceRef(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
inode=os.fstat(namespace_fd).st_ino,
|
inode=os.fstat(namespace_fd).st_ino,
|
||||||
|
|
@ -249,8 +249,8 @@ def test_open_wg_bindspace_nests_resource_lifetimes(
|
||||||
Nested WG interfaces must exit before their bindspace capability.
|
Nested WG interfaces must exit before their bindspace capability.
|
||||||
|
|
||||||
Fake two interface layers over one bindspace. Clear the caller's
|
Fake two interface layers over one bindspace. Clear the caller's
|
||||||
mutable layer list at bindspace entry, then cancel from inside the
|
mutable layer list at bindspace entry, then cancel from inside
|
||||||
yielded application scope and checkpoint. The trace proves the
|
the yielded application scope and checkpoint. The trace proves
|
||||||
stack snapshots its declaration before entry, layers enter
|
stack snapshots its declaration before entry, layers enter
|
||||||
outermost-first, cancellation exits them inside-out, and the live
|
outermost-first, cancellation exits them inside-out, and the live
|
||||||
bindspace remains available through every interface exit.
|
bindspace remains available through every interface exit.
|
||||||
|
|
@ -261,16 +261,16 @@ def test_open_wg_bindspace_nests_resource_lifetimes(
|
||||||
tuple[
|
tuple[
|
||||||
WGTunnelSpec,
|
WGTunnelSpec,
|
||||||
WGInterfaceConfig,
|
WGInterfaceConfig,
|
||||||
BindspaceHandle,
|
Bindspace,
|
||||||
_tunnel.WGRole,
|
_tunnel.WGRole,
|
||||||
]
|
]
|
||||||
] = []
|
] = []
|
||||||
bindspace_spec: BindspaceSpec = BindspaceSpec(
|
bindspace_spec: BindspaceSpec = BindspaceSpec(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
)
|
)
|
||||||
bindspace: BindspaceHandle = BindspaceHandle(
|
bindspace: Bindspace = Bindspace(
|
||||||
spec=bindspace_spec,
|
spec=bindspace_spec,
|
||||||
identity=BindspaceIdentity(
|
ref=BindspaceRef(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key=None,
|
key=None,
|
||||||
inode=1,
|
inode=1,
|
||||||
|
|
@ -302,7 +302,7 @@ def test_open_wg_bindspace_nests_resource_lifetimes(
|
||||||
@acm
|
@acm
|
||||||
async def fake_open_bindspace(
|
async def fake_open_bindspace(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
) -> AsyncIterator[BindspaceHandle]:
|
) -> AsyncIterator[Bindspace]:
|
||||||
'''
|
'''
|
||||||
Yield the stand-in bindspace and record its full lifetime.
|
Yield the stand-in bindspace and record its full lifetime.
|
||||||
|
|
||||||
|
|
@ -319,19 +319,19 @@ def test_open_wg_bindspace_nests_resource_lifetimes(
|
||||||
async def fake_open_wg_iface(
|
async def fake_open_wg_iface(
|
||||||
spec: WGTunnelSpec,
|
spec: WGTunnelSpec,
|
||||||
config: WGInterfaceConfig,
|
config: WGInterfaceConfig,
|
||||||
handle: BindspaceHandle,
|
bindspace_arg: Bindspace,
|
||||||
role: _tunnel.WGRole,
|
role: _tunnel.WGRole,
|
||||||
) -> AsyncIterator[WGTunnelSpec]:
|
) -> AsyncIterator[WGTunnelSpec]:
|
||||||
'''
|
'''
|
||||||
Record one interface's arguments and nested lifetime.
|
Record one interface's arguments and nested lifetime.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
calls.append((spec, config, handle, role))
|
calls.append((spec, config, bindspace_arg, role))
|
||||||
events.append(f'{spec.iface}-enter')
|
events.append(f'{spec.iface}-enter')
|
||||||
try:
|
try:
|
||||||
yield spec
|
yield spec
|
||||||
finally:
|
finally:
|
||||||
assert handle is bindspace
|
assert bindspace_arg is bindspace
|
||||||
events.append(f'{spec.iface}-exit')
|
events.append(f'{spec.iface}-exit')
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
|
|
@ -355,8 +355,8 @@ def test_open_wg_bindspace_nests_resource_lifetimes(
|
||||||
bindspace_spec,
|
bindspace_spec,
|
||||||
layers,
|
layers,
|
||||||
'dial',
|
'dial',
|
||||||
) as handle:
|
) as opened_bindspace:
|
||||||
assert handle is bindspace
|
assert opened_bindspace is bindspace
|
||||||
events.append('yield')
|
events.append('yield')
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@ here to avoid circular imports; use direct module paths for those.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from ._bindspace import (
|
from ._bindspace import (
|
||||||
BindspaceHandle as BindspaceHandle,
|
Bindspace as Bindspace,
|
||||||
BindspaceIdentity as BindspaceIdentity,
|
|
||||||
BindspaceKind as BindspaceKind,
|
BindspaceKind as BindspaceKind,
|
||||||
BindspaceLifecycle as BindspaceLifecycle,
|
BindspaceLifecycle as BindspaceLifecycle,
|
||||||
BindspaceOwnership as BindspaceOwnership,
|
BindspaceOwnership as BindspaceOwnership,
|
||||||
|
BindspaceRef as BindspaceRef,
|
||||||
BindspaceSpec as BindspaceSpec,
|
BindspaceSpec as BindspaceSpec,
|
||||||
CURRENT_NETNS as CURRENT_NETNS,
|
CURRENT_NETNS as CURRENT_NETNS,
|
||||||
attach_netns as attach_netns,
|
attach_netns as attach_netns,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
# License along with this program. If not, see
|
# License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
'''
|
'''
|
||||||
Serializable bindspace declarations and live capability handles.
|
Serializable bindspace declarations and live capabilities.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -91,7 +91,7 @@ def _validate_bindspace_key(
|
||||||
Reject empty or path-like platform-resource names.
|
Reject empty or path-like platform-resource names.
|
||||||
|
|
||||||
`None` is valid. Spell it `CURRENT_NETNS` for
|
`None` is valid. Spell it `CURRENT_NETNS` for
|
||||||
`BindspaceSpec.key`; `BindspaceIdentity.key = None` records an
|
`BindspaceSpec.key`; `BindspaceRef.key = None` records an
|
||||||
unnamed realized netns.
|
unnamed realized netns.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
@ -145,16 +145,17 @@ class BindspaceSpec(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BindspaceIdentity(
|
class BindspaceRef(
|
||||||
msgspec.Struct,
|
msgspec.Struct,
|
||||||
frozen=True,
|
frozen=True,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Serializable stable identity of one realized bindspace.
|
Serializable, non-owning ref to one realized bindspace.
|
||||||
|
|
||||||
`.key` is an optional, mutable namespace name. `.inode` is the
|
`.key` is an optional mutable namespace locator. `.inode` is a
|
||||||
required kernel identity which remains stable after rename or
|
host-local kernel fingerprint which remains stable while the
|
||||||
unlink.
|
resource exists or a live `Bindspace` pins it. This ref grants no
|
||||||
|
authority and cannot reopen the resource by itself.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
kind: BindspaceKind
|
kind: BindspaceKind
|
||||||
|
|
@ -163,14 +164,14 @@ class BindspaceIdentity(
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
'''
|
'''
|
||||||
Require a stable platform identity and an optional name.
|
Require a host-local resource inode and an optional locator.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
_validate_bindspace_kind(self.kind)
|
_validate_bindspace_kind(self.kind)
|
||||||
_validate_bindspace_key(
|
_validate_bindspace_key(
|
||||||
self.kind,
|
self.kind,
|
||||||
self.key,
|
self.key,
|
||||||
'BindspaceIdentity.key',
|
'BindspaceRef.key',
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
type(self.inode) is not int
|
type(self.inode) is not int
|
||||||
|
|
@ -178,23 +179,23 @@ class BindspaceIdentity(
|
||||||
self.inode <= 0
|
self.inode <= 0
|
||||||
):
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'`BindspaceIdentity.inode` must be a positive `int`!'
|
'`BindspaceRef.inode` must be a positive `int`!'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BindspaceHandle(
|
class Bindspace(
|
||||||
ProcessLocal,
|
ProcessLocal,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Process-local capability for one live realized bindspace.
|
Process-local capability for one live realized bindspace.
|
||||||
|
|
||||||
`ProcessLocal` provides compact typed storage plus a default
|
`ProcessLocal` provides compact typed storage plus a default
|
||||||
wire-encoding guard. Explicit FD transfer and handle construction
|
wire-encoding guard. `Bindspace` construction and explicit FD
|
||||||
belong to the supervisor's spawn/bootstrap path.
|
transfer belong to the supervisor's spawn/bootstrap path.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
spec: BindspaceSpec
|
spec: BindspaceSpec
|
||||||
identity: BindspaceIdentity
|
ref: BindspaceRef
|
||||||
namespace_fd: int|None
|
namespace_fd: int|None
|
||||||
ownership: BindspaceOwnership
|
ownership: BindspaceOwnership
|
||||||
|
|
||||||
|
|
@ -204,23 +205,23 @@ class BindspaceHandle(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
spec: BindspaceSpec = self.spec
|
spec: BindspaceSpec = self.spec
|
||||||
identity: BindspaceIdentity = self.identity
|
ref: BindspaceRef = self.ref
|
||||||
namespace_fd: int|None = self.namespace_fd
|
namespace_fd: int|None = self.namespace_fd
|
||||||
ownership: BindspaceOwnership = self.ownership
|
ownership: BindspaceOwnership = self.ownership
|
||||||
|
|
||||||
if spec.kind != identity.kind:
|
if spec.kind != ref.kind:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'`BindspaceSpec.kind` does not match '
|
'`BindspaceSpec.kind` does not match '
|
||||||
'`BindspaceIdentity.kind`!'
|
'`BindspaceRef.kind`!'
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
spec.key is not None
|
spec.key is not None
|
||||||
and
|
and
|
||||||
spec.key != identity.key
|
spec.key != ref.key
|
||||||
):
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'`BindspaceSpec.key` does not match '
|
'`BindspaceSpec.key` does not match '
|
||||||
'`BindspaceIdentity.key`!'
|
'`BindspaceRef.key`!'
|
||||||
)
|
)
|
||||||
if ownership not in get_args(BindspaceOwnership):
|
if ownership not in get_args(BindspaceOwnership):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|
@ -246,20 +247,20 @@ class BindspaceHandle(
|
||||||
'`namespace_fd` must be non-negative or `None`!'
|
'`namespace_fd` must be non-negative or `None`!'
|
||||||
)
|
)
|
||||||
fd_inode: int = os.fstat(namespace_fd).st_ino
|
fd_inode: int = os.fstat(namespace_fd).st_ino
|
||||||
if identity.inode != fd_inode:
|
if ref.inode != fd_inode:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Namespace FD inode {fd_inode} does not match '
|
f'Namespace FD inode {fd_inode} does not match '
|
||||||
f'identity inode {identity.inode}!'
|
f'reference inode {ref.inode}!'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
'''
|
'''
|
||||||
Render capability identity without dereferencing its FD.
|
Render the capability ref without dereferencing its FD.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
return (
|
return (
|
||||||
f'{type(self).__name__}('
|
f'{type(self).__name__}('
|
||||||
f'identity={self.identity!r}, '
|
f'ref={self.ref!r}, '
|
||||||
f'ownership={self.ownership!r}, '
|
f'ownership={self.ownership!r}, '
|
||||||
f'namespace_fd={self.namespace_fd!r})'
|
f'namespace_fd={self.namespace_fd!r})'
|
||||||
)
|
)
|
||||||
|
|
@ -269,7 +270,7 @@ class BindspaceHandle(
|
||||||
async def _pin_netns(
|
async def _pin_netns(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
ownership: BindspaceOwnership,
|
ownership: BindspaceOwnership,
|
||||||
) -> AsyncIterator[BindspaceHandle]:
|
) -> AsyncIterator[Bindspace]:
|
||||||
'''
|
'''
|
||||||
Pin one existing Linux network namespace with explicit ownership.
|
Pin one existing Linux network namespace with explicit ownership.
|
||||||
|
|
||||||
|
|
@ -286,18 +287,18 @@ async def _pin_netns(
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
inode: int = os.fstat(namespace_fd).st_ino
|
inode: int = os.fstat(namespace_fd).st_ino
|
||||||
identity: BindspaceIdentity = BindspaceIdentity(
|
ref: BindspaceRef = BindspaceRef(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key=key,
|
key=key,
|
||||||
inode=inode,
|
inode=inode,
|
||||||
)
|
)
|
||||||
handle: BindspaceHandle = BindspaceHandle(
|
bindspace: Bindspace = Bindspace(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
identity=identity,
|
ref=ref,
|
||||||
namespace_fd=namespace_fd,
|
namespace_fd=namespace_fd,
|
||||||
ownership=ownership,
|
ownership=ownership,
|
||||||
)
|
)
|
||||||
yield handle
|
yield bindspace
|
||||||
finally:
|
finally:
|
||||||
os.close(namespace_fd)
|
os.close(namespace_fd)
|
||||||
|
|
||||||
|
|
@ -305,7 +306,7 @@ async def _pin_netns(
|
||||||
@acm
|
@acm
|
||||||
async def attach_netns(
|
async def attach_netns(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
) -> AsyncIterator[BindspaceHandle]:
|
) -> AsyncIterator[Bindspace]:
|
||||||
'''
|
'''
|
||||||
Borrow and pin one existing Linux network namespace.
|
Borrow and pin one existing Linux network namespace.
|
||||||
|
|
||||||
|
|
@ -326,8 +327,8 @@ async def attach_netns(
|
||||||
async with _pin_netns(
|
async with _pin_netns(
|
||||||
spec,
|
spec,
|
||||||
ownership='borrowed',
|
ownership='borrowed',
|
||||||
) as handle:
|
) as bindspace:
|
||||||
yield handle
|
yield bindspace
|
||||||
|
|
||||||
|
|
||||||
def _create_netns(
|
def _create_netns(
|
||||||
|
|
@ -367,7 +368,7 @@ def _remove_netns(
|
||||||
@acm
|
@acm
|
||||||
async def open_netns(
|
async def open_netns(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
) -> AsyncIterator[BindspaceHandle]:
|
) -> AsyncIterator[Bindspace]:
|
||||||
'''
|
'''
|
||||||
Create, pin and own one named Linux network namespace.
|
Create, pin and own one named Linux network namespace.
|
||||||
|
|
||||||
|
|
@ -404,8 +405,8 @@ async def open_netns(
|
||||||
async with _pin_netns(
|
async with _pin_netns(
|
||||||
spec,
|
spec,
|
||||||
ownership='owned',
|
ownership='owned',
|
||||||
) as handle:
|
) as bindspace:
|
||||||
yield handle
|
yield bindspace
|
||||||
finally:
|
finally:
|
||||||
if created:
|
if created:
|
||||||
with trio.CancelScope(shield=True):
|
with trio.CancelScope(shield=True):
|
||||||
|
|
@ -419,7 +420,7 @@ async def open_netns(
|
||||||
@acm
|
@acm
|
||||||
async def open_bindspace(
|
async def open_bindspace(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
) -> AsyncIterator[BindspaceHandle]:
|
) -> AsyncIterator[Bindspace]:
|
||||||
'''
|
'''
|
||||||
Dispatch one declared bindspace lifecycle.
|
Dispatch one declared bindspace lifecycle.
|
||||||
|
|
||||||
|
|
@ -428,8 +429,8 @@ async def open_bindspace(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if spec.lifecycle == 'attach':
|
if spec.lifecycle == 'attach':
|
||||||
async with attach_netns(spec) as handle:
|
async with attach_netns(spec) as bindspace:
|
||||||
yield handle
|
yield bindspace
|
||||||
else:
|
else:
|
||||||
async with open_netns(spec) as handle:
|
async with open_netns(spec) as bindspace:
|
||||||
yield handle
|
yield bindspace
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ import trio
|
||||||
|
|
||||||
from ..msg._local import ProcessLocal
|
from ..msg._local import ProcessLocal
|
||||||
from ._bindspace import (
|
from ._bindspace import (
|
||||||
BindspaceHandle,
|
Bindspace,
|
||||||
BindspaceSpec,
|
BindspaceSpec,
|
||||||
open_bindspace,
|
open_bindspace,
|
||||||
)
|
)
|
||||||
|
|
@ -463,7 +463,7 @@ def _wg_iface_settings(
|
||||||
def _sync_create_wg_iface(
|
def _sync_create_wg_iface(
|
||||||
spec: WGTunnelSpec,
|
spec: WGTunnelSpec,
|
||||||
config: WGInterfaceConfig,
|
config: WGInterfaceConfig,
|
||||||
bindspace: BindspaceHandle,
|
bindspace: Bindspace,
|
||||||
listen_port: int|None,
|
listen_port: int|None,
|
||||||
peers: tuple[dict[str, object], ...],
|
peers: tuple[dict[str, object], ...],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -559,7 +559,7 @@ def _sync_create_wg_iface(
|
||||||
|
|
||||||
def _sync_remove_wg_iface(
|
def _sync_remove_wg_iface(
|
||||||
spec: WGTunnelSpec,
|
spec: WGTunnelSpec,
|
||||||
bindspace: BindspaceHandle,
|
bindspace: Bindspace,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Remove one owned WireGuard iface when it still exists.
|
Remove one owned WireGuard iface when it still exists.
|
||||||
|
|
@ -593,7 +593,7 @@ def _sync_remove_wg_iface(
|
||||||
async def open_wg_iface(
|
async def open_wg_iface(
|
||||||
spec: WGTunnelSpec,
|
spec: WGTunnelSpec,
|
||||||
config: WGInterfaceConfig,
|
config: WGInterfaceConfig,
|
||||||
bindspace: BindspaceHandle,
|
bindspace: Bindspace,
|
||||||
role: WGRole,
|
role: WGRole,
|
||||||
) -> AsyncIterator[WGTunnelSpec]:
|
) -> AsyncIterator[WGTunnelSpec]:
|
||||||
'''
|
'''
|
||||||
|
|
@ -637,7 +637,7 @@ async def open_wg_bindspace(
|
||||||
bindspace_spec: BindspaceSpec,
|
bindspace_spec: BindspaceSpec,
|
||||||
layers: Sequence[tuple[WGTunnelSpec, WGInterfaceConfig]],
|
layers: Sequence[tuple[WGTunnelSpec, WGInterfaceConfig]],
|
||||||
role: WGRole,
|
role: WGRole,
|
||||||
) -> AsyncIterator[BindspaceHandle]:
|
) -> AsyncIterator[Bindspace]:
|
||||||
'''
|
'''
|
||||||
Open one bindspace and its ordered WireGuard interface stack.
|
Open one bindspace and its ordered WireGuard interface stack.
|
||||||
|
|
||||||
|
|
@ -662,7 +662,7 @@ async def open_wg_bindspace(
|
||||||
...,
|
...,
|
||||||
] = tuple(layers)
|
] = tuple(layers)
|
||||||
async with AsyncExitStack() as stack:
|
async with AsyncExitStack() as stack:
|
||||||
bindspace: BindspaceHandle = await (
|
bindspace: Bindspace = await (
|
||||||
stack.enter_async_context(
|
stack.enter_async_context(
|
||||||
open_bindspace(bindspace_spec)
|
open_bindspace(bindspace_spec)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue