Compare commits
No commits in common. "d067505addc69690962232e80fbdff939dabebd1" and "d130431ca1981429c45e3c9d738cd4408e84211e" have entirely different histories.
d067505add
...
d130431ca1
|
|
@ -1,54 +0,0 @@
|
||||||
---
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: tractor-addr-unpacking
|
|
||||||
timestamp: 2026-08-26T03:05:34Z
|
|
||||||
git_ref: d130431c
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260826T030534Z_d130431c_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
After committing the reviewed bindspace/interface stack, the human
|
|
||||||
asked to continue with the next established Layer C step.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Retained a serializable realized `BindspaceRef` on frozen tunnelled
|
|
||||||
address declarations without carrying the process-local `Bindspace`
|
|
||||||
capability. The namespace API remains tuple-shaped, using the declared
|
|
||||||
key before realization and stable inode afterward. Existing transport
|
|
||||||
peeling and unrealized serialization remain unchanged.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/discovery/_tunnel.py` - realized bindspace-ref retention and
|
|
||||||
immutable annotation method.
|
|
||||||
- `tests/discovery/test_tunnelled_addr.py` - ref, compatibility and
|
|
||||||
mismatch coverage.
|
|
||||||
- `tests/ipc/test_server_tunnel_boundary.py` - real listener diagnostic
|
|
||||||
coverage for the realized inode.
|
|
||||||
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - realized namespace
|
|
||||||
visibility contract.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human accepted and committed the preceding reviewed composition
|
|
||||||
boundary, then requested the next step. Their stated preference for
|
|
||||||
end-to-end tests informed use of the existing real TCP listener path to
|
|
||||||
verify endpoint/server diagnostics, while focused unit coverage handles
|
|
||||||
metadata invariants without pyroute2 overhead. During review, the human
|
|
||||||
chose the final unshipped terminology: `BindspaceRef` for the
|
|
||||||
serializable non-owning reference, `Bindspace` for the live
|
|
||||||
process-local capability, `.ref` for that capability's reference, and
|
|
||||||
`bindspace_ref` at the tunnel declaration API.
|
|
||||||
They then requested the prerequisite model rename as a separate commit
|
|
||||||
before this feature. The human also replaced the module-level helper
|
|
||||||
with `TunnelledAddress.with_bindspace_ref()` and requested inline
|
|
||||||
msgspec encode/decode expressions in the serialization assertions. The
|
|
||||||
human chose not to add a second typed `.namespace` projection, and
|
|
||||||
requested native tagged `TunnelledAddress` decoding remain as a separate
|
|
||||||
design-plan follow-up. The agent applied those human-directed changes;
|
|
||||||
no direct manual source edits were observed.
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
---
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-26T03:05:34Z
|
|
||||||
git_ref: d130431c
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw output - retain realized bindspace identity
|
|
||||||
|
|
||||||
After committing WireGuard bindspace composition, the human requested
|
|
||||||
the next incremental Layer C change.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/discovery/_tunnel.py tractor/discovery/__init__.py tests/discovery/test_tunnelled_addr.py tests/ipc/test_server_tunnel_boundary.py ai/tpt-backends/03_wg_tunnel_bindspace.md`
|
|
||||||
|
|
||||||
Added optional `BindspaceIdentity` metadata to frozen
|
|
||||||
`TunnelledAddress` declarations and a pure
|
|
||||||
`with_bindspace_identity()` annotation helper. Unrealized declarations
|
|
||||||
retain their prior serialized shape. Realized declarations retain only
|
|
||||||
serializable key/inode identity, never the FD-bearing capability.
|
|
||||||
|
|
||||||
The existing `.namespace` tuple contract remains compatible:
|
|
||||||
unrealized declarations report `(kind, key)`, while realized
|
|
||||||
declarations report the stable `(kind, inode)`. Name mismatches between
|
|
||||||
the tunnel declaration and realized bindspace are rejected.
|
|
||||||
|
|
||||||
Unit coverage verifies immutability, serialization, delegation and
|
|
||||||
mismatch handling. The existing real TCP listener test proves endpoint
|
|
||||||
and server diagnostics expose the retained inode without implying that
|
|
||||||
the process entered that namespace. Ruff passed and focused tunnel,
|
|
||||||
listener and bindspace coverage passed 30 tests.
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
---
|
|
||||||
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.
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
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,10 +115,9 @@ 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` request and host-local `BindspaceRef`. A scoped,
|
`BindspaceSpec` and a scoped, non-serializable `BindspaceHandle`;
|
||||||
non-serializable `Bindspace` carries that ref and owns the FD/lifetime
|
the latter owns namespace identity/FD/lifetime and is consumed at
|
||||||
used during spawn bootstrap before a concrete address reaches
|
spawn bootstrap before a concrete address reaches transport bind.
|
||||||
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 capabilities. A manager crash fails closed:
|
underlying namespace/capability handles. 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 three bindspace representations with deliberately different roles
|
Keep two bindspace representations with deliberately different
|
||||||
and lifetimes:
|
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 BindspaceRef(msgspec.Struct, frozen=True):
|
class BindspaceIdentity(msgspec.Struct, frozen=True):
|
||||||
'''Wire-safe, non-owning ref to the realized resource.'''
|
'''Stable identity of the realized platform resource.'''
|
||||||
kind: str
|
kind: str
|
||||||
key: str|None # mutable name, absent after unlink
|
key: str|None # mutable name, absent after unlink
|
||||||
inode: int # host-local Linux nsfs fingerprint
|
inode: int # stable Linux namespace identity
|
||||||
|
|
||||||
|
|
||||||
class Bindspace(ProcessLocal):
|
class BindspaceHandle(ProcessLocal):
|
||||||
'''Scoped, non-serializable capability for one live bindspace.'''
|
'''Scoped, non-serializable capability for one live bindspace.'''
|
||||||
spec: BindspaceSpec
|
spec: BindspaceSpec
|
||||||
ref: BindspaceRef
|
identity: BindspaceIdentity
|
||||||
namespace_fd: int|None
|
namespace_fd: int|None
|
||||||
ownership: Literal['owned', 'borrowed']
|
ownership: Literal['owned', 'borrowed']
|
||||||
|
|
||||||
|
|
@ -429,7 +429,7 @@ class Bindspace(ProcessLocal):
|
||||||
@acm
|
@acm
|
||||||
async def open_bindspace(
|
async def open_bindspace(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
) -> AsyncGenerator[Bindspace, None]:
|
) -> AsyncGenerator[BindspaceHandle, None]:
|
||||||
'''
|
'''
|
||||||
Provision/borrow one bindspace and yield its live capability.
|
Provision/borrow one bindspace and yield its live capability.
|
||||||
|
|
||||||
|
|
@ -437,17 +437,15 @@ async def open_bindspace(
|
||||||
```
|
```
|
||||||
|
|
||||||
The initial model limits `BindspaceKind` to `netns` while preserving
|
The initial model limits `BindspaceKind` to `netns` while preserving
|
||||||
the required role split. `BindspaceSpec` is the requested resource and
|
the required lifetime split. `BindspaceSpec` and
|
||||||
lifecycle policy. `BindspaceRef` is a serializable, non-owning,
|
`BindspaceIdentity` are frozen msgspec structs which cross
|
||||||
host-local record of the resource that was actually opened; it can be
|
config/spawn serialization. `BindspaceHandle` also uses msgspec's
|
||||||
compared or logged, but cannot reopen, pin or enter that resource.
|
generic struct storage by inheriting the global
|
||||||
`Bindspace` is the live capability and uses msgspec's generic struct
|
`tractor.msg.ProcessLocal` marker. Its hidden unsupported sentinel
|
||||||
storage by inheriting the global `tractor.msg.ProcessLocal` marker. Its
|
blocks direct and nested default msgspec encoding without a recursive
|
||||||
hidden unsupported sentinel blocks direct and nested default msgspec
|
IPC hot-path scan. The handle validates any supplied FD against
|
||||||
encoding without a recursive IPC hot-path scan. The live bindspace
|
`BindspaceIdentity.inode`; explicit FD transfer belongs to the
|
||||||
validates any supplied FD against `BindspaceRef.inode`; explicit FD
|
supervisor bootstrap path. An FD avoids name-resolution TOCTOU,
|
||||||
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.
|
||||||
|
|
@ -461,7 +459,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 a `BindspaceRef` from the opened FD, yields
|
`/var/run/netns`. It derives identity 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
|
||||||
|
|
@ -476,8 +474,7 @@ 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 live bindspace to replace an overlay while preserving every
|
use the handle to replace an overlay while preserving every tunnel:
|
||||||
tunnel:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async with open_bindspace(
|
async with open_bindspace(
|
||||||
|
|
@ -506,13 +503,13 @@ tunnel/bindspace layer:
|
||||||
@acm
|
@acm
|
||||||
async def open_netns(
|
async def open_netns(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
) -> AsyncGenerator[Bindspace, None]: ...
|
) -> AsyncGenerator[BindspaceHandle, None]: ...
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def open_wg_iface(
|
async def open_wg_iface(
|
||||||
spec: WGTunnelSpec,
|
spec: WGTunnelSpec,
|
||||||
config: WGInterfaceConfig,
|
config: WGInterfaceConfig,
|
||||||
bindspace: Bindspace,
|
bindspace: BindspaceHandle,
|
||||||
role: Literal['listen', 'dial'],
|
role: Literal['listen', 'dial'],
|
||||||
) -> AsyncGenerator[WGTunnelSpec, None]: ...
|
) -> AsyncGenerator[WGTunnelSpec, None]: ...
|
||||||
```
|
```
|
||||||
|
|
@ -545,7 +542,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 `Bindspace` for endpoint
|
`AsyncExitStack`, and yields the live `BindspaceHandle` 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.
|
||||||
|
|
@ -557,12 +554,8 @@ composed maddr can name a server source or client destination (§5.4).
|
||||||
|
|
||||||
### 5.3 `Address.namespace`, at last
|
### 5.3 `Address.namespace`, at last
|
||||||
|
|
||||||
- an unrealized `TunnelledAddress.namespace` reports its declared name
|
- `TunnelledAddress.namespace` → `(kind, id)` e.g.
|
||||||
as `(kind, key)`, e.g. `('netns', 'tractor-wg0')`;
|
`('netns', 'tractor-wg0')`.
|
||||||
- `TunnelledAddress.with_bindspace_ref()` returns a frozen declaration
|
|
||||||
annotated with `bindspace.ref`, never the FD-bearing `Bindspace`.
|
|
||||||
Its `.namespace` reports `(kind, inode)` so the
|
|
||||||
realized ref remains stable across rename or unlink;
|
|
||||||
- existing plain backends implement it explicitly as `None`, so the
|
- existing plain backends implement it explicitly as `None`, so the
|
||||||
Protocol does not lie and tunnel delegation needs no `getattr()`
|
Protocol does not lie and tunnel delegation needs no `getattr()`
|
||||||
fallback.
|
fallback.
|
||||||
|
|
@ -571,19 +564,12 @@ composed maddr can name a server source or client destination (§5.4).
|
||||||
`Server.pformat()` expose that value as the cheapest proof the layer
|
`Server.pformat()` expose that value as the cheapest proof the layer
|
||||||
is wired.
|
is wired.
|
||||||
|
|
||||||
Deferred follow-ups:
|
|
||||||
|
|
||||||
- add native tagged encoding for the complete `TunnelledAddress` graph,
|
|
||||||
including its concrete overlay-address union, tunnel-spec union and
|
|
||||||
optional `BindspaceRef`. Once that codec exists, tests should perform
|
|
||||||
typed roundtrips instead of inspecting an untyped decoded payload.
|
|
||||||
|
|
||||||
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)` reference
|
`ip netns identify` probe establish the useful `(key, inode)` identity
|
||||||
record. Layer C should move that shape into `BindspaceRef`, avoid a
|
pair. Layer C should move that shape into `BindspaceIdentity`, avoid a
|
||||||
subprocess where netlink/procfs suffices, and hold the namespace FD in
|
subprocess where netlink/procfs suffices, and hold the namespace FD in
|
||||||
`Bindspace` to pin the referenced resource.
|
`BindspaceHandle` to pin the identity.
|
||||||
|
|
||||||
### 5.4 the netns/process reality — read this before designing
|
### 5.4 the netns/process reality — read this before designing
|
||||||
|
|
||||||
|
|
@ -626,7 +612,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 `Bindspace` context.
|
the parent/supervisor's `BindspaceHandle` 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`;
|
||||||
|
|
@ -655,7 +641,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 capability;
|
- borrowed dial/actor-wide bindspaces only release their handle;
|
||||||
- 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
|
||||||
|
|
@ -727,7 +713,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 `Bindspace`; borrowed bindspaces never remove resources |
|
| dial path tears down a shared actor bindspace | encode ownership in `BindspaceHandle`; borrowed handles 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, reference and live-capability contracts.
|
Bindspace declaration, identity 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 (
|
||||||
Bindspace,
|
BindspaceHandle,
|
||||||
|
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 refs must cross actor IPC.
|
Spawn configuration and realized identity 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 refs survive
|
concrete types, proving names and stable inode identity survive
|
||||||
without carrying any process-local capability state.
|
without carrying any process-local capability state.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
values: tuple[
|
values: tuple[
|
||||||
BindspaceSpec|BindspaceRef,
|
BindspaceSpec|BindspaceIdentity,
|
||||||
...,
|
...,
|
||||||
] = (
|
] = (
|
||||||
BindspaceSpec(
|
BindspaceSpec(
|
||||||
|
|
@ -45,16 +45,16 @@ def test_bindspace_declarations_roundtrip() -> None:
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
lifecycle='open',
|
lifecycle='open',
|
||||||
),
|
),
|
||||||
BindspaceRef(
|
BindspaceIdentity(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
inode=1234,
|
inode=1234,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
value: BindspaceSpec|BindspaceRef
|
value: BindspaceSpec|BindspaceIdentity
|
||||||
for value in values:
|
for value in values:
|
||||||
encoded: bytes = msgspec.msgpack.encode(value)
|
encoded: bytes = msgspec.msgpack.encode(value)
|
||||||
decoded: BindspaceSpec|BindspaceRef = (
|
decoded: BindspaceSpec|BindspaceIdentity = (
|
||||||
msgspec.msgpack.decode(
|
msgspec.msgpack.decode(
|
||||||
encoded,
|
encoded,
|
||||||
type=type(value),
|
type=type(value),
|
||||||
|
|
@ -63,17 +63,16 @@ def test_bindspace_declarations_roundtrip() -> None:
|
||||||
assert decoded == value
|
assert decoded == value
|
||||||
|
|
||||||
|
|
||||||
def test_bindspace_pins_local_capability(
|
def test_bindspace_handle_pins_local_capability(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
A live bindspace pins one exact FD and realized ref.
|
A live handle pins one exact FD and realized identity.
|
||||||
|
|
||||||
Open a stand-in platform FD, record its inode in the realized
|
Open a stand-in platform handle, record its inode in the realized
|
||||||
ref and construct an owned capability. Prove the generic
|
identity 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
|
encode ordinary fields is not authority to transfer the handle.
|
||||||
bindspace.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
token_path: Path = tmp_path / 'bindspace'
|
token_path: Path = tmp_path / 'bindspace'
|
||||||
|
|
@ -87,36 +86,36 @@ def test_bindspace_pins_local_capability(
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
lifecycle='open',
|
lifecycle='open',
|
||||||
)
|
)
|
||||||
ref: BindspaceRef = BindspaceRef(
|
identity: BindspaceIdentity = BindspaceIdentity(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
inode=inode,
|
inode=inode,
|
||||||
)
|
)
|
||||||
bindspace: Bindspace = Bindspace(
|
handle: BindspaceHandle = BindspaceHandle(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
ref=ref,
|
identity=identity,
|
||||||
namespace_fd=namespace_fd,
|
namespace_fd=namespace_fd,
|
||||||
ownership='owned',
|
ownership='owned',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert bindspace.spec is spec
|
assert handle.spec is spec
|
||||||
assert bindspace.ref is ref
|
assert handle.identity is identity
|
||||||
assert bindspace.namespace_fd == namespace_file.fileno()
|
assert handle.namespace_fd == namespace_file.fileno()
|
||||||
assert bindspace.ownership == 'owned'
|
assert handle.ownership == 'owned'
|
||||||
assert isinstance(bindspace, msgspec.Struct)
|
assert isinstance(handle, msgspec.Struct)
|
||||||
assert isinstance(bindspace, ProcessLocal)
|
assert isinstance(handle, ProcessLocal)
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
TypeError,
|
TypeError,
|
||||||
match='_ProcessLocalToken.*unsupported',
|
match='_ProcessLocalToken.*unsupported',
|
||||||
):
|
):
|
||||||
msgspec.msgpack.encode(bindspace)
|
msgspec.msgpack.encode(handle)
|
||||||
|
|
||||||
|
|
||||||
def test_bindspace_rejects_mismatched_ref(
|
def test_bindspace_handle_rejects_mismatched_identity(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
A name or inode mismatch would make a bindspace stale authority.
|
A name or inode mismatch would make a handle 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
|
||||||
|
|
@ -129,39 +128,39 @@ def test_bindspace_rejects_mismatched_ref(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
)
|
)
|
||||||
# Keep ownership and FD fixed so only the ref changes below.
|
# Keep ownership and FD fixed so only identity 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: BindspaceRef = BindspaceRef(
|
wrong_name: BindspaceIdentity = BindspaceIdentity(
|
||||||
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.*Ref.key',
|
match='Spec.key.*Identity.key',
|
||||||
):
|
):
|
||||||
Bindspace(
|
BindspaceHandle(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
ref=wrong_name,
|
identity=wrong_name,
|
||||||
namespace_fd=namespace_fd,
|
namespace_fd=namespace_fd,
|
||||||
ownership=ownership,
|
ownership=ownership,
|
||||||
)
|
)
|
||||||
|
|
||||||
wrong_inode: BindspaceRef = BindspaceRef(
|
wrong_inode: BindspaceIdentity = BindspaceIdentity(
|
||||||
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.*reference inode',
|
match='FD inode.*identity inode',
|
||||||
):
|
):
|
||||||
Bindspace(
|
BindspaceHandle(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
ref=wrong_inode,
|
identity=wrong_inode,
|
||||||
namespace_fd=namespace_fd,
|
namespace_fd=namespace_fd,
|
||||||
ownership=ownership,
|
ownership=ownership,
|
||||||
)
|
)
|
||||||
|
|
@ -171,14 +170,14 @@ def test_bindspace_rejects_mismatched_ref(
|
||||||
('model', 'kwargs', 'match'),
|
('model', 'kwargs', 'match'),
|
||||||
(
|
(
|
||||||
pytest.param(
|
pytest.param(
|
||||||
BindspaceRef,
|
BindspaceIdentity,
|
||||||
{
|
{
|
||||||
'kind': 'netns',
|
'kind': 'netns',
|
||||||
'key': None,
|
'key': None,
|
||||||
'inode': None,
|
'inode': None,
|
||||||
},
|
},
|
||||||
'must be a positive `int`',
|
'must be a positive `int`',
|
||||||
id='ref-requires-inode',
|
id='identity-requires-inode',
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
BindspaceSpec,
|
BindspaceSpec,
|
||||||
|
|
@ -187,14 +186,14 @@ def test_bindspace_rejects_mismatched_ref(
|
||||||
id='spec-rejects-kind',
|
id='spec-rejects-kind',
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
BindspaceRef,
|
BindspaceIdentity,
|
||||||
{
|
{
|
||||||
'kind': 'vrf',
|
'kind': 'vrf',
|
||||||
'key': 'blue',
|
'key': 'blue',
|
||||||
'inode': 1234,
|
'inode': 1234,
|
||||||
},
|
},
|
||||||
'Unsupported bindspace kind',
|
'Unsupported bindspace kind',
|
||||||
id='ref-rejects-kind',
|
id='identity-rejects-kind',
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
BindspaceSpec,
|
BindspaceSpec,
|
||||||
|
|
@ -225,19 +224,19 @@ def test_bindspace_rejects_mismatched_ref(
|
||||||
id='spec-rejects-lifecycle',
|
id='spec-rejects-lifecycle',
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
BindspaceRef,
|
BindspaceIdentity,
|
||||||
{
|
{
|
||||||
'kind': 'netns',
|
'kind': 'netns',
|
||||||
'key': '',
|
'key': '',
|
||||||
'inode': 1234,
|
'inode': 1234,
|
||||||
},
|
},
|
||||||
'BindspaceRef.key',
|
'BindspaceIdentity.key',
|
||||||
id='ref-rejects-empty-key',
|
id='identity-rejects-empty-key',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_bindspace_models_reject_invalid_values(
|
def test_bindspace_models_reject_invalid_values(
|
||||||
model: type[BindspaceSpec]|type[BindspaceRef],
|
model: type[BindspaceSpec]|type[BindspaceIdentity],
|
||||||
kwargs: dict[str, object],
|
kwargs: dict[str, object],
|
||||||
match: str,
|
match: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -246,7 +245,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
|
||||||
refs or provisioning instructions into spawn configuration.
|
identity or provisioning instructions into spawn configuration.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
with pytest.raises(ValueError, match=match):
|
with pytest.raises(ValueError, match=match):
|
||||||
|
|
@ -261,7 +260,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 bindspace records its
|
Open `/proc/self/ns/net`, prove the yielded handle 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.
|
||||||
|
|
@ -276,15 +275,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 bindspace:
|
async with open_bindspace(spec) as handle:
|
||||||
namespace_fd: int|None = bindspace.namespace_fd
|
namespace_fd: int|None = handle.namespace_fd
|
||||||
assert namespace_fd is not None
|
assert namespace_fd is not None
|
||||||
assert bindspace.spec is spec
|
assert handle.spec is spec
|
||||||
assert bindspace.ref.key is None
|
assert handle.identity.key is None
|
||||||
assert bindspace.ref.inode == os.fstat(
|
assert handle.identity.inode == os.fstat(
|
||||||
namespace_fd
|
namespace_fd
|
||||||
).st_ino
|
).st_ino
|
||||||
assert bindspace.ownership == 'borrowed'
|
assert handle.ownership == 'borrowed'
|
||||||
return namespace_fd
|
return namespace_fd
|
||||||
|
|
||||||
namespace_fd: int = trio.run(main)
|
namespace_fd: int = trio.run(main)
|
||||||
|
|
@ -325,12 +324,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 bindspace:
|
async with attach_netns(spec) as handle:
|
||||||
namespace_fd: int|None = bindspace.namespace_fd
|
namespace_fd: int|None = handle.namespace_fd
|
||||||
assert namespace_fd is not None
|
assert namespace_fd is not None
|
||||||
assert bindspace.ref.key == 'tractor-wg0'
|
assert handle.identity.key == 'tractor-wg0'
|
||||||
assert bindspace.ref.inode == netns_path.stat().st_ino
|
assert handle.identity.inode == netns_path.stat().st_ino
|
||||||
assert bindspace.ownership == 'borrowed'
|
assert handle.ownership == 'borrowed'
|
||||||
return namespace_fd
|
return namespace_fd
|
||||||
|
|
||||||
namespace_fd: int = trio.run(main)
|
namespace_fd: int = trio.run(main)
|
||||||
|
|
@ -390,7 +389,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 ref while it exists, then prove FD closure
|
yielded FD and identity while it exists, then prove FD closure
|
||||||
precedes resource removal when the context exits.
|
precedes resource removal when the context exits.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
@ -445,11 +444,11 @@ def test_open_netns_owns_lifecycle(
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
lifecycle='open',
|
lifecycle='open',
|
||||||
)
|
)
|
||||||
async with open_bindspace(spec) as bindspace:
|
async with open_bindspace(spec) as handle:
|
||||||
fd: int|None = bindspace.namespace_fd
|
fd: int|None = handle.namespace_fd
|
||||||
assert fd is not None
|
assert fd is not None
|
||||||
assert bindspace.ownership == 'owned'
|
assert handle.ownership == 'owned'
|
||||||
assert bindspace.ref.inode == os.fstat(fd).st_ino
|
assert handle.identity.inode == os.fstat(fd).st_ino
|
||||||
namespace_fds.append(fd)
|
namespace_fds.append(fd)
|
||||||
events.append('yield')
|
events.append('yield')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import msgspec
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tractor.discovery import (
|
from tractor.discovery import (
|
||||||
BindspaceRef,
|
|
||||||
TunnelledAddress,
|
TunnelledAddress,
|
||||||
WGTunnelSpec,
|
WGTunnelSpec,
|
||||||
mb_pubkey,
|
mb_pubkey,
|
||||||
|
|
@ -197,71 +196,6 @@ def test_namespace_comes_from_the_tunnel(
|
||||||
assert in_ns.namespace == ('netns', 'wg-test')
|
assert in_ns.namespace == ('netns', 'wg-test')
|
||||||
|
|
||||||
|
|
||||||
def test_realized_namespace_uses_stable_ref(
|
|
||||||
overlay: TCPAddress,
|
|
||||||
) -> None:
|
|
||||||
'''
|
|
||||||
Realization must retain a stable ref without mutating the maddr.
|
|
||||||
|
|
||||||
Build an unrealized named declaration, annotate it with the
|
|
||||||
matching realized key and inode, and prove the frozen original is
|
|
||||||
unchanged. The annotated copy must preserve transport delegation
|
|
||||||
and expose the stable inode through `.namespace`. Direct msgspec
|
|
||||||
encoding also proves only serializable ref metadata was retained.
|
|
||||||
|
|
||||||
'''
|
|
||||||
declared: TunnelledAddress = TunnelledAddress(
|
|
||||||
overlay=overlay,
|
|
||||||
tunnel=WGTunnelSpec(
|
|
||||||
peer_pubkey=_PUBKEY,
|
|
||||||
netns='wg-test',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
ref: BindspaceRef = BindspaceRef(
|
|
||||||
kind='netns',
|
|
||||||
key='wg-test',
|
|
||||||
inode=1234,
|
|
||||||
)
|
|
||||||
realized: TunnelledAddress = declared.with_bindspace_ref(
|
|
||||||
ref,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert declared.bindspace_ref is None
|
|
||||||
assert realized.bindspace_ref is ref
|
|
||||||
assert realized.namespace == ('netns', 1234)
|
|
||||||
assert realized.overlay is declared.overlay
|
|
||||||
assert realized.tunnel is declared.tunnel
|
|
||||||
assert realized.unwrap() == declared.unwrap()
|
|
||||||
assert realized.bindspace == declared.bindspace
|
|
||||||
|
|
||||||
declared_payload: dict[str, object] = msgspec.msgpack.decode(
|
|
||||||
msgspec.msgpack.encode(declared)
|
|
||||||
)
|
|
||||||
assert 'bindspace_ref' not in declared_payload
|
|
||||||
|
|
||||||
decoded: dict[str, object] = msgspec.msgpack.decode(
|
|
||||||
msgspec.msgpack.encode(realized)
|
|
||||||
)
|
|
||||||
assert decoded['bindspace_ref'] == {
|
|
||||||
'kind': 'netns',
|
|
||||||
'key': 'wg-test',
|
|
||||||
'inode': 1234,
|
|
||||||
}
|
|
||||||
|
|
||||||
mismatched: BindspaceRef = BindspaceRef(
|
|
||||||
kind='netns',
|
|
||||||
key='other-netns',
|
|
||||||
inode=5678,
|
|
||||||
)
|
|
||||||
with pytest.raises(
|
|
||||||
ValueError,
|
|
||||||
match='wg-test.*other-netns',
|
|
||||||
):
|
|
||||||
declared.with_bindspace_ref(
|
|
||||||
mismatched,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_strip_tunnels(
|
def test_strip_tunnels(
|
||||||
tunnelled: TunnelledAddress,
|
tunnelled: TunnelledAddress,
|
||||||
overlay: TCPAddress,
|
overlay: TCPAddress,
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ import pytest
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from tractor.discovery import (
|
from tractor.discovery import (
|
||||||
Bindspace,
|
BindspaceHandle,
|
||||||
BindspaceRef,
|
BindspaceIdentity,
|
||||||
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: Bindspace,
|
bindspace: BindspaceHandle,
|
||||||
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: Bindspace,
|
bindspace: BindspaceHandle,
|
||||||
) -> 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: Bindspace = Bindspace(
|
bindspace: BindspaceHandle = BindspaceHandle(
|
||||||
spec=bindspace_spec,
|
spec=bindspace_spec,
|
||||||
ref=BindspaceRef(
|
identity=BindspaceIdentity(
|
||||||
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
|
mutable layer list at bindspace entry, then cancel from inside the
|
||||||
the yielded application scope and checkpoint. The trace proves
|
yielded application scope and checkpoint. The trace proves the
|
||||||
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,
|
||||||
Bindspace,
|
BindspaceHandle,
|
||||||
_tunnel.WGRole,
|
_tunnel.WGRole,
|
||||||
]
|
]
|
||||||
] = []
|
] = []
|
||||||
bindspace_spec: BindspaceSpec = BindspaceSpec(
|
bindspace_spec: BindspaceSpec = BindspaceSpec(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
)
|
)
|
||||||
bindspace: Bindspace = Bindspace(
|
bindspace: BindspaceHandle = BindspaceHandle(
|
||||||
spec=bindspace_spec,
|
spec=bindspace_spec,
|
||||||
ref=BindspaceRef(
|
identity=BindspaceIdentity(
|
||||||
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[Bindspace]:
|
) -> AsyncIterator[BindspaceHandle]:
|
||||||
'''
|
'''
|
||||||
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,
|
||||||
bindspace_arg: Bindspace,
|
handle: BindspaceHandle,
|
||||||
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, bindspace_arg, role))
|
calls.append((spec, config, handle, role))
|
||||||
events.append(f'{spec.iface}-enter')
|
events.append(f'{spec.iface}-enter')
|
||||||
try:
|
try:
|
||||||
yield spec
|
yield spec
|
||||||
finally:
|
finally:
|
||||||
assert bindspace_arg is bindspace
|
assert handle 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 opened_bindspace:
|
) as handle:
|
||||||
assert opened_bindspace is bindspace
|
assert handle is bindspace
|
||||||
events.append('yield')
|
events.append('yield')
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ from __future__ import annotations
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from tractor.discovery import (
|
from tractor.discovery import (
|
||||||
BindspaceRef,
|
|
||||||
TunnelledAddress,
|
TunnelledAddress,
|
||||||
WGTunnelSpec,
|
WGTunnelSpec,
|
||||||
tunnels_of,
|
tunnels_of,
|
||||||
|
|
@ -26,9 +25,8 @@ def test_server_peels_before_endpoint_construction() -> None:
|
||||||
instead of the TCP backend. Start a real listener from the
|
instead of the TCP backend. Start a real listener from the
|
||||||
wrapper, assert the resulting `Endpoint` contains only a resolved
|
wrapper, assert the resulting `Endpoint` contains only a resolved
|
||||||
`TCPAddress`. Prove `Endpoint.declared_addr` still retains the
|
`TCPAddress`. Prove `Endpoint.declared_addr` still retains the
|
||||||
original declaration and realized bindspace ref for
|
original tunnel namespace for diagnostics and the future
|
||||||
diagnostics. This retained metadata does not claim the listener
|
bindspace lifecycle.
|
||||||
process entered that namespace.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
overlay = TCPAddress('127.0.0.1', 0)
|
overlay = TCPAddress('127.0.0.1', 0)
|
||||||
|
|
@ -40,19 +38,11 @@ def test_server_peels_before_endpoint_construction() -> None:
|
||||||
netns='actor-net',
|
netns='actor-net',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ref: BindspaceRef = BindspaceRef(
|
|
||||||
kind='netns',
|
|
||||||
key='actor-net',
|
|
||||||
inode=1234,
|
|
||||||
)
|
|
||||||
declared: TunnelledAddress = tunnelled.with_bindspace_ref(
|
|
||||||
ref,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with open_ipc_server() as server:
|
async with open_ipc_server() as server:
|
||||||
eps = await server.listen_on(
|
eps = await server.listen_on(
|
||||||
accept_addrs=[declared],
|
accept_addrs=[tunnelled],
|
||||||
)
|
)
|
||||||
assert len(eps) == 1
|
assert len(eps) == 1
|
||||||
endpoint = eps[0]
|
endpoint = eps[0]
|
||||||
|
|
@ -61,9 +51,9 @@ def test_server_peels_before_endpoint_construction() -> None:
|
||||||
_, host, port = endpoint.addr.unwrap()
|
_, host, port = endpoint.addr.unwrap()
|
||||||
assert host == overlay.unwrap()[1]
|
assert host == overlay.unwrap()[1]
|
||||||
assert port > 0
|
assert port > 0
|
||||||
assert endpoint.addr is not declared
|
assert endpoint.addr is not tunnelled
|
||||||
assert endpoint.declared_addr is declared
|
assert endpoint.declared_addr is tunnelled
|
||||||
namespace: tuple[str, int] = ('netns', 1234)
|
namespace: tuple[str, str] = ('netns', 'actor-net')
|
||||||
assert endpoint.namespace == namespace
|
assert endpoint.namespace == namespace
|
||||||
endpoint_repr: str = endpoint.pformat()
|
endpoint_repr: str = endpoint.pformat()
|
||||||
server_repr: str = server.pformat()
|
server_repr: str = server.pformat()
|
||||||
|
|
@ -71,9 +61,9 @@ def test_server_peels_before_endpoint_construction() -> None:
|
||||||
assert expected_namespace in endpoint_repr
|
assert expected_namespace in endpoint_repr
|
||||||
assert ' |_namespaces:' in server_repr
|
assert ' |_namespaces:' in server_repr
|
||||||
assert 'netns' in server_repr
|
assert 'netns' in server_repr
|
||||||
assert '1234' in server_repr
|
assert 'actor-net' in server_repr
|
||||||
assert tunnels_of(declared) == (
|
assert tunnels_of(tunnelled) == (
|
||||||
declared.tunnel,
|
tunnelled.tunnel,
|
||||||
)
|
)
|
||||||
|
|
||||||
server.cancel()
|
server.cancel()
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@ here to avoid circular imports; use direct module paths for those.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from ._bindspace import (
|
from ._bindspace import (
|
||||||
Bindspace as Bindspace,
|
BindspaceHandle as BindspaceHandle,
|
||||||
|
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 capabilities.
|
Serializable bindspace declarations and live capability handles.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
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`; `BindspaceRef.key = None` records an
|
`BindspaceSpec.key`; `BindspaceIdentity.key = None` records an
|
||||||
unnamed realized netns.
|
unnamed realized netns.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
@ -145,17 +145,16 @@ class BindspaceSpec(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BindspaceRef(
|
class BindspaceIdentity(
|
||||||
msgspec.Struct,
|
msgspec.Struct,
|
||||||
frozen=True,
|
frozen=True,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Serializable, non-owning ref to one realized bindspace.
|
Serializable stable identity of one realized bindspace.
|
||||||
|
|
||||||
`.key` is an optional mutable namespace locator. `.inode` is a
|
`.key` is an optional, mutable namespace name. `.inode` is the
|
||||||
host-local kernel fingerprint which remains stable while the
|
required kernel identity which remains stable after rename or
|
||||||
resource exists or a live `Bindspace` pins it. This ref grants no
|
unlink.
|
||||||
authority and cannot reopen the resource by itself.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
kind: BindspaceKind
|
kind: BindspaceKind
|
||||||
|
|
@ -164,14 +163,14 @@ class BindspaceRef(
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
'''
|
'''
|
||||||
Require a host-local resource inode and an optional locator.
|
Require a stable platform identity and an optional name.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
_validate_bindspace_kind(self.kind)
|
_validate_bindspace_kind(self.kind)
|
||||||
_validate_bindspace_key(
|
_validate_bindspace_key(
|
||||||
self.kind,
|
self.kind,
|
||||||
self.key,
|
self.key,
|
||||||
'BindspaceRef.key',
|
'BindspaceIdentity.key',
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
type(self.inode) is not int
|
type(self.inode) is not int
|
||||||
|
|
@ -179,23 +178,23 @@ class BindspaceRef(
|
||||||
self.inode <= 0
|
self.inode <= 0
|
||||||
):
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'`BindspaceRef.inode` must be a positive `int`!'
|
'`BindspaceIdentity.inode` must be a positive `int`!'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Bindspace(
|
class BindspaceHandle(
|
||||||
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. `Bindspace` construction and explicit FD
|
wire-encoding guard. Explicit FD transfer and handle construction
|
||||||
transfer belong to the supervisor's spawn/bootstrap path.
|
belong to the supervisor's spawn/bootstrap path.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
spec: BindspaceSpec
|
spec: BindspaceSpec
|
||||||
ref: BindspaceRef
|
identity: BindspaceIdentity
|
||||||
namespace_fd: int|None
|
namespace_fd: int|None
|
||||||
ownership: BindspaceOwnership
|
ownership: BindspaceOwnership
|
||||||
|
|
||||||
|
|
@ -205,23 +204,23 @@ class Bindspace(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
spec: BindspaceSpec = self.spec
|
spec: BindspaceSpec = self.spec
|
||||||
ref: BindspaceRef = self.ref
|
identity: BindspaceIdentity = self.identity
|
||||||
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 != ref.kind:
|
if spec.kind != identity.kind:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'`BindspaceSpec.kind` does not match '
|
'`BindspaceSpec.kind` does not match '
|
||||||
'`BindspaceRef.kind`!'
|
'`BindspaceIdentity.kind`!'
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
spec.key is not None
|
spec.key is not None
|
||||||
and
|
and
|
||||||
spec.key != ref.key
|
spec.key != identity.key
|
||||||
):
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'`BindspaceSpec.key` does not match '
|
'`BindspaceSpec.key` does not match '
|
||||||
'`BindspaceRef.key`!'
|
'`BindspaceIdentity.key`!'
|
||||||
)
|
)
|
||||||
if ownership not in get_args(BindspaceOwnership):
|
if ownership not in get_args(BindspaceOwnership):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|
@ -247,20 +246,20 @@ class Bindspace(
|
||||||
'`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 ref.inode != fd_inode:
|
if identity.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'reference inode {ref.inode}!'
|
f'identity inode {identity.inode}!'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
'''
|
'''
|
||||||
Render the capability ref without dereferencing its FD.
|
Render capability identity without dereferencing its FD.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
return (
|
return (
|
||||||
f'{type(self).__name__}('
|
f'{type(self).__name__}('
|
||||||
f'ref={self.ref!r}, '
|
f'identity={self.identity!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})'
|
||||||
)
|
)
|
||||||
|
|
@ -270,7 +269,7 @@ class Bindspace(
|
||||||
async def _pin_netns(
|
async def _pin_netns(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
ownership: BindspaceOwnership,
|
ownership: BindspaceOwnership,
|
||||||
) -> AsyncIterator[Bindspace]:
|
) -> AsyncIterator[BindspaceHandle]:
|
||||||
'''
|
'''
|
||||||
Pin one existing Linux network namespace with explicit ownership.
|
Pin one existing Linux network namespace with explicit ownership.
|
||||||
|
|
||||||
|
|
@ -287,18 +286,18 @@ async def _pin_netns(
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
inode: int = os.fstat(namespace_fd).st_ino
|
inode: int = os.fstat(namespace_fd).st_ino
|
||||||
ref: BindspaceRef = BindspaceRef(
|
identity: BindspaceIdentity = BindspaceIdentity(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key=key,
|
key=key,
|
||||||
inode=inode,
|
inode=inode,
|
||||||
)
|
)
|
||||||
bindspace: Bindspace = Bindspace(
|
handle: BindspaceHandle = BindspaceHandle(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
ref=ref,
|
identity=identity,
|
||||||
namespace_fd=namespace_fd,
|
namespace_fd=namespace_fd,
|
||||||
ownership=ownership,
|
ownership=ownership,
|
||||||
)
|
)
|
||||||
yield bindspace
|
yield handle
|
||||||
finally:
|
finally:
|
||||||
os.close(namespace_fd)
|
os.close(namespace_fd)
|
||||||
|
|
||||||
|
|
@ -306,7 +305,7 @@ async def _pin_netns(
|
||||||
@acm
|
@acm
|
||||||
async def attach_netns(
|
async def attach_netns(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
) -> AsyncIterator[Bindspace]:
|
) -> AsyncIterator[BindspaceHandle]:
|
||||||
'''
|
'''
|
||||||
Borrow and pin one existing Linux network namespace.
|
Borrow and pin one existing Linux network namespace.
|
||||||
|
|
||||||
|
|
@ -327,8 +326,8 @@ async def attach_netns(
|
||||||
async with _pin_netns(
|
async with _pin_netns(
|
||||||
spec,
|
spec,
|
||||||
ownership='borrowed',
|
ownership='borrowed',
|
||||||
) as bindspace:
|
) as handle:
|
||||||
yield bindspace
|
yield handle
|
||||||
|
|
||||||
|
|
||||||
def _create_netns(
|
def _create_netns(
|
||||||
|
|
@ -368,7 +367,7 @@ def _remove_netns(
|
||||||
@acm
|
@acm
|
||||||
async def open_netns(
|
async def open_netns(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
) -> AsyncIterator[Bindspace]:
|
) -> AsyncIterator[BindspaceHandle]:
|
||||||
'''
|
'''
|
||||||
Create, pin and own one named Linux network namespace.
|
Create, pin and own one named Linux network namespace.
|
||||||
|
|
||||||
|
|
@ -405,8 +404,8 @@ async def open_netns(
|
||||||
async with _pin_netns(
|
async with _pin_netns(
|
||||||
spec,
|
spec,
|
||||||
ownership='owned',
|
ownership='owned',
|
||||||
) as bindspace:
|
) as handle:
|
||||||
yield bindspace
|
yield handle
|
||||||
finally:
|
finally:
|
||||||
if created:
|
if created:
|
||||||
with trio.CancelScope(shield=True):
|
with trio.CancelScope(shield=True):
|
||||||
|
|
@ -420,7 +419,7 @@ async def open_netns(
|
||||||
@acm
|
@acm
|
||||||
async def open_bindspace(
|
async def open_bindspace(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
) -> AsyncIterator[Bindspace]:
|
) -> AsyncIterator[BindspaceHandle]:
|
||||||
'''
|
'''
|
||||||
Dispatch one declared bindspace lifecycle.
|
Dispatch one declared bindspace lifecycle.
|
||||||
|
|
||||||
|
|
@ -429,8 +428,8 @@ async def open_bindspace(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if spec.lifecycle == 'attach':
|
if spec.lifecycle == 'attach':
|
||||||
async with attach_netns(spec) as bindspace:
|
async with attach_netns(spec) as handle:
|
||||||
yield bindspace
|
yield handle
|
||||||
else:
|
else:
|
||||||
async with open_netns(spec) as bindspace:
|
async with open_netns(spec) as handle:
|
||||||
yield bindspace
|
yield handle
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,7 @@ import trio
|
||||||
|
|
||||||
from ..msg._local import ProcessLocal
|
from ..msg._local import ProcessLocal
|
||||||
from ._bindspace import (
|
from ._bindspace import (
|
||||||
Bindspace,
|
BindspaceHandle,
|
||||||
BindspaceRef,
|
|
||||||
BindspaceSpec,
|
BindspaceSpec,
|
||||||
open_bindspace,
|
open_bindspace,
|
||||||
)
|
)
|
||||||
|
|
@ -461,7 +460,7 @@ def _wg_iface_settings(
|
||||||
def _sync_create_wg_iface(
|
def _sync_create_wg_iface(
|
||||||
spec: WGTunnelSpec,
|
spec: WGTunnelSpec,
|
||||||
config: WGInterfaceConfig,
|
config: WGInterfaceConfig,
|
||||||
bindspace: Bindspace,
|
bindspace: BindspaceHandle,
|
||||||
listen_port: int|None,
|
listen_port: int|None,
|
||||||
peers: tuple[dict[str, object], ...],
|
peers: tuple[dict[str, object], ...],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -557,7 +556,7 @@ def _sync_create_wg_iface(
|
||||||
|
|
||||||
def _sync_remove_wg_iface(
|
def _sync_remove_wg_iface(
|
||||||
spec: WGTunnelSpec,
|
spec: WGTunnelSpec,
|
||||||
bindspace: Bindspace,
|
bindspace: BindspaceHandle,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Remove one owned WireGuard iface when it still exists.
|
Remove one owned WireGuard iface when it still exists.
|
||||||
|
|
@ -591,7 +590,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: Bindspace,
|
bindspace: BindspaceHandle,
|
||||||
role: WGRole,
|
role: WGRole,
|
||||||
) -> AsyncIterator[WGTunnelSpec]:
|
) -> AsyncIterator[WGTunnelSpec]:
|
||||||
'''
|
'''
|
||||||
|
|
@ -635,7 +634,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[Bindspace]:
|
) -> AsyncIterator[BindspaceHandle]:
|
||||||
'''
|
'''
|
||||||
Open one bindspace and its ordered WireGuard interface stack.
|
Open one bindspace and its ordered WireGuard interface stack.
|
||||||
|
|
||||||
|
|
@ -660,7 +659,7 @@ async def open_wg_bindspace(
|
||||||
...,
|
...,
|
||||||
] = tuple(layers)
|
] = tuple(layers)
|
||||||
async with AsyncExitStack() as stack:
|
async with AsyncExitStack() as stack:
|
||||||
bindspace: Bindspace = await (
|
bindspace: BindspaceHandle = await (
|
||||||
stack.enter_async_context(
|
stack.enter_async_context(
|
||||||
open_bindspace(bindspace_spec)
|
open_bindspace(bindspace_spec)
|
||||||
)
|
)
|
||||||
|
|
@ -859,7 +858,6 @@ def _wg_proto_code() -> int:
|
||||||
class TunnelledAddress(
|
class TunnelledAddress(
|
||||||
msgspec.Struct,
|
msgspec.Struct,
|
||||||
frozen=True,
|
frozen=True,
|
||||||
omit_defaults=True,
|
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
An `Address` annotated with the tunnel it must be reached
|
An `Address` annotated with the tunnel it must be reached
|
||||||
|
|
@ -874,32 +872,6 @@ class TunnelledAddress(
|
||||||
'''
|
'''
|
||||||
overlay: Address|TunnelledAddress
|
overlay: Address|TunnelledAddress
|
||||||
tunnel: TunnelSpec
|
tunnel: TunnelSpec
|
||||||
bindspace_ref: BindspaceRef|None = None
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
'''
|
|
||||||
Validate the retained ref against the tunnel declaration.
|
|
||||||
|
|
||||||
'''
|
|
||||||
ref: BindspaceRef|None = self.bindspace_ref
|
|
||||||
if ref is None:
|
|
||||||
return
|
|
||||||
if not isinstance(ref, BindspaceRef):
|
|
||||||
raise TypeError(
|
|
||||||
'`TunnelledAddress.bindspace_ref` must be a '
|
|
||||||
'`BindspaceRef` or `None`!'
|
|
||||||
)
|
|
||||||
|
|
||||||
declared_netns: str|None = self.tunnel.netns
|
|
||||||
if (
|
|
||||||
declared_netns is not None
|
|
||||||
and
|
|
||||||
ref.key != declared_netns
|
|
||||||
):
|
|
||||||
raise ValueError(
|
|
||||||
f'Declared netns {declared_netns!r} does not match '
|
|
||||||
f'realized bindspace key {ref.key!r}!'
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---- delegated, so the runtime can't tell the difference ----
|
# ---- delegated, so the runtime can't tell the difference ----
|
||||||
|
|
||||||
|
|
@ -939,35 +911,14 @@ class TunnelledAddress(
|
||||||
@property
|
@property
|
||||||
def namespace(self) -> tuple[str, str|int]|None:
|
def namespace(self) -> tuple[str, str|int]|None:
|
||||||
'''
|
'''
|
||||||
Return the realized ref or declared tunnel netns.
|
The tunnel's netns, when it declares one.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
ref: BindspaceRef|None = self.bindspace_ref
|
|
||||||
if ref is not None:
|
|
||||||
return (
|
|
||||||
ref.kind,
|
|
||||||
ref.inode,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (netns := self.tunnel.netns) is None:
|
if (netns := self.tunnel.netns) is None:
|
||||||
return self.overlay.namespace
|
return self.overlay.namespace
|
||||||
|
|
||||||
return ('netns', netns)
|
return ('netns', netns)
|
||||||
|
|
||||||
def with_bindspace_ref(
|
|
||||||
self,
|
|
||||||
ref: BindspaceRef,
|
|
||||||
) -> TunnelledAddress:
|
|
||||||
'''
|
|
||||||
Return a copy retaining one realized bindspace ref.
|
|
||||||
|
|
||||||
'''
|
|
||||||
realized: TunnelledAddress = msgspec.structs.replace(
|
|
||||||
self,
|
|
||||||
bindspace_ref=ref,
|
|
||||||
)
|
|
||||||
return realized
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
f'{type(self).__name__}(\n'
|
f'{type(self).__name__}(\n'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue