Dispatch explicit bindspace lifecycles
Add serialized `BindspaceSpec.lifecycle` policy and dispatch it through `open_bindspace()` without inferring ownership from transport role. Deats, - distinguish borrowed `attach` from owned `open` policy - validate handle ownership against the declared lifecycle - share policy-neutral FD pinning between both netns contexts - reject unsupported lifecycle values before side effects - exercise both dispatcher branches and owned cancellation cleanup Prompt-IO: ai/prompt-io/opencode/20260825T191845Z_5b2a064a_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
parent
5b2a064ae8
commit
b973e78c2a
|
|
@ -0,0 +1,40 @@
|
||||||
|
---
|
||||||
|
model: gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
session: tractor-addr-unpacking
|
||||||
|
timestamp: 2026-08-25T19:18:45Z
|
||||||
|
git_ref: 5b2a064a
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20260825T191845Z_5b2a064a_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
The human pointed out that the agent had produced no changes after the
|
||||||
|
previous commit and expected the next incremental implementation.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Corrected the sequencing mistake and added explicit serialized
|
||||||
|
bindspace lifecycle policy plus generic `open_bindspace()` dispatch.
|
||||||
|
Borrowed and owned netns contexts share a policy-neutral FD-pinning
|
||||||
|
primitive while validating their public lifecycle before side effects.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tractor/discovery/_bindspace.py` - lifecycle policy, ownership
|
||||||
|
invariants, shared pinning and dispatch.
|
||||||
|
- `tractor/discovery/__init__.py` - public policy and dispatcher exports.
|
||||||
|
- `tests/discovery/test_bindspace.py` - lifecycle serialization,
|
||||||
|
validation and both dispatcher branches.
|
||||||
|
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - explicit lifecycle
|
||||||
|
policy independent of transport role.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
The human identified that the agent had accidentally repeated a
|
||||||
|
summary of already committed work instead of implementing the next
|
||||||
|
slice. That correction directly caused this lifecycle/dispatcher
|
||||||
|
change to be implemented. The agent wrote the source changes; no
|
||||||
|
direct manual edits were observed.
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
model: gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
timestamp: 2026-08-25T19:18:45Z
|
||||||
|
git_ref: 5b2a064a
|
||||||
|
diff_cmd: git diff HEAD~1..HEAD
|
||||||
|
---
|
||||||
|
|
||||||
|
# Raw output - dispatch explicit bindspace lifecycle
|
||||||
|
|
||||||
|
The human noticed the agent had re-reported already committed work
|
||||||
|
without changing the worktree. The agent confirmed the sequencing
|
||||||
|
error and proceeded to the actual next Layer C slice.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tractor/discovery/_bindspace.py tractor/discovery/__init__.py tests/discovery/test_bindspace.py ai/tpt-backends/03_wg_tunnel_bindspace.md`
|
||||||
|
|
||||||
|
Added serialized `BindspaceSpec.lifecycle` policy with explicit
|
||||||
|
`attach` and `open` choices. Added `open_bindspace()` dispatch without
|
||||||
|
inferring resource ownership from listen/dial role.
|
||||||
|
|
||||||
|
Refactored FD attachment into policy-neutral `_pin_netns()` so
|
||||||
|
borrowed and owned public lifecycles can share identity pinning while
|
||||||
|
enforcing their own policy before side effects. Handle construction
|
||||||
|
also verifies lifecycle and resulting ownership agree.
|
||||||
|
|
||||||
|
One composition error introduced during implementation was caught by
|
||||||
|
the focused test: owned creation initially called the newly guarded
|
||||||
|
public attach API. `_pin_netns()` fixed that layering error. Ruff and
|
||||||
|
lock checks passed; discovery plus message coverage passed 138 tests
|
||||||
|
with 2 xpasses.
|
||||||
|
|
@ -408,6 +408,7 @@ class BindspaceSpec(msgspec.Struct, frozen=True):
|
||||||
'''Serializable spawn/config declaration.'''
|
'''Serializable spawn/config declaration.'''
|
||||||
kind: str # `netns`, later `vrf`, ...
|
kind: str # `netns`, later `vrf`, ...
|
||||||
key: str|None # requested name/key, if any
|
key: str|None # requested name/key, if any
|
||||||
|
lifecycle: Literal['attach', 'open']
|
||||||
|
|
||||||
|
|
||||||
class BindspaceIdentity(msgspec.Struct, frozen=True):
|
class BindspaceIdentity(msgspec.Struct, frozen=True):
|
||||||
|
|
@ -428,8 +429,6 @@ class BindspaceHandle(ProcessLocal):
|
||||||
@acm
|
@acm
|
||||||
async def open_bindspace(
|
async def open_bindspace(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
*,
|
|
||||||
role: Literal['listen', 'dial'],
|
|
||||||
) -> AsyncGenerator[BindspaceHandle, None]:
|
) -> AsyncGenerator[BindspaceHandle, None]:
|
||||||
'''
|
'''
|
||||||
Provision/borrow one bindspace and yield its live capability.
|
Provision/borrow one bindspace and yield its live capability.
|
||||||
|
|
@ -451,6 +450,12 @@ 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.
|
||||||
|
|
||||||
|
`BindspaceSpec.lifecycle` is explicit serialized policy:
|
||||||
|
`'attach'` borrows an existing resource and `'open'` creates/owns one.
|
||||||
|
`open_bindspace()` dispatches that policy by bindspace kind. Never
|
||||||
|
infer it from a listen/dial role: either role may use pre-provisioned
|
||||||
|
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
|
||||||
|
|
@ -474,7 +479,6 @@ use the handle to replace an overlay while preserving every tunnel:
|
||||||
```python
|
```python
|
||||||
async with open_bindspace(
|
async with open_bindspace(
|
||||||
bindspace_spec,
|
bindspace_spec,
|
||||||
role='listen',
|
|
||||||
) as bindspace:
|
) as bindspace:
|
||||||
listen_decl = declared_addr.get_random(
|
listen_decl = declared_addr.get_random(
|
||||||
bindspace=bindspace,
|
bindspace=bindspace,
|
||||||
|
|
@ -499,7 +503,6 @@ tunnel/bindspace layer:
|
||||||
@acm
|
@acm
|
||||||
async def open_netns(
|
async def open_netns(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
role: Literal['listen', 'dial'],
|
|
||||||
) -> AsyncGenerator[BindspaceHandle, None]: ...
|
) -> AsyncGenerator[BindspaceHandle, None]: ...
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
|
|
@ -519,9 +522,10 @@ it already returns
|
||||||
`dict[str, list[Address]]|dict[...]` return for tunnelled
|
`dict[str, list[Address]]|dict[...]` return for tunnelled
|
||||||
entries. Extend it to carry the tunnel stack, not to *enter* it.
|
entries. Extend it to carry the tunnel stack, not to *enter* it.
|
||||||
|
|
||||||
The caller supplies `role`; do not infer it from maddr shape. The same
|
The caller supplies `role` to tunnel-resource contexts such as
|
||||||
composed maddr can name a server source or client destination, and the
|
`open_wg_iface()`; do not infer it from maddr shape. Bindspace
|
||||||
required local provisioning/ownership differs (§5.4).
|
lifecycle remains the independent explicit policy above. The same
|
||||||
|
composed maddr can name a server source or client destination (§5.4).
|
||||||
|
|
||||||
### 5.3 `Address.namespace`, at last
|
### 5.3 `Address.namespace`, at last
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ from tractor.discovery import (
|
||||||
BindspaceSpec,
|
BindspaceSpec,
|
||||||
CURRENT_NETNS,
|
CURRENT_NETNS,
|
||||||
attach_netns,
|
attach_netns,
|
||||||
|
open_bindspace,
|
||||||
open_netns,
|
open_netns,
|
||||||
)
|
)
|
||||||
from tractor.discovery import _bindspace
|
from tractor.discovery import _bindspace
|
||||||
|
|
@ -39,7 +40,11 @@ def test_bindspace_declarations_roundtrip() -> None:
|
||||||
BindspaceSpec|BindspaceIdentity,
|
BindspaceSpec|BindspaceIdentity,
|
||||||
...,
|
...,
|
||||||
] = (
|
] = (
|
||||||
BindspaceSpec(kind='netns', key='tractor-wg0'),
|
BindspaceSpec(
|
||||||
|
kind='netns',
|
||||||
|
key='tractor-wg0',
|
||||||
|
lifecycle='open',
|
||||||
|
),
|
||||||
BindspaceIdentity(
|
BindspaceIdentity(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
|
|
@ -79,6 +84,7 @@ def test_bindspace_handle_pins_local_capability(
|
||||||
spec: BindspaceSpec = BindspaceSpec(
|
spec: BindspaceSpec = BindspaceSpec(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
|
lifecycle='open',
|
||||||
)
|
)
|
||||||
identity: BindspaceIdentity = BindspaceIdentity(
|
identity: BindspaceIdentity = BindspaceIdentity(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
|
|
@ -207,6 +213,16 @@ def test_bindspace_handle_rejects_mismatched_identity(
|
||||||
'BindspaceSpec.key',
|
'BindspaceSpec.key',
|
||||||
id='spec-rejects-empty-key',
|
id='spec-rejects-empty-key',
|
||||||
),
|
),
|
||||||
|
pytest.param(
|
||||||
|
BindspaceSpec,
|
||||||
|
{
|
||||||
|
'kind': 'netns',
|
||||||
|
'key': 'tractor-wg0',
|
||||||
|
'lifecycle': 'replace',
|
||||||
|
},
|
||||||
|
'Unsupported bindspace lifecycle',
|
||||||
|
id='spec-rejects-lifecycle',
|
||||||
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
BindspaceIdentity,
|
BindspaceIdentity,
|
||||||
{
|
{
|
||||||
|
|
@ -240,7 +256,7 @@ def test_bindspace_models_reject_invalid_values(
|
||||||
sys.platform != 'linux',
|
sys.platform != 'linux',
|
||||||
reason='Linux netns API',
|
reason='Linux netns API',
|
||||||
)
|
)
|
||||||
def test_attach_current_netns_borrows_and_closes() -> None:
|
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.
|
||||||
|
|
||||||
|
|
@ -259,7 +275,7 @@ def test_attach_current_netns_borrows_and_closes() -> None:
|
||||||
kind='netns',
|
kind='netns',
|
||||||
)
|
)
|
||||||
assert spec.key is CURRENT_NETNS
|
assert spec.key is CURRENT_NETNS
|
||||||
async with attach_netns(spec) as handle:
|
async with open_bindspace(spec) as handle:
|
||||||
namespace_fd: int|None = handle.namespace_fd
|
namespace_fd: int|None = handle.namespace_fd
|
||||||
assert namespace_fd is not None
|
assert namespace_fd is not None
|
||||||
assert handle.spec is spec
|
assert handle.spec is spec
|
||||||
|
|
@ -426,8 +442,9 @@ def test_open_netns_owns_lifecycle(
|
||||||
spec: BindspaceSpec = BindspaceSpec(
|
spec: BindspaceSpec = BindspaceSpec(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
|
lifecycle='open',
|
||||||
)
|
)
|
||||||
async with open_netns(spec) as handle:
|
async with open_bindspace(spec) as handle:
|
||||||
fd: int|None = handle.namespace_fd
|
fd: int|None = handle.namespace_fd
|
||||||
assert fd is not None
|
assert fd is not None
|
||||||
assert handle.ownership == 'owned'
|
assert handle.ownership == 'owned'
|
||||||
|
|
@ -503,6 +520,7 @@ def test_open_netns_shields_cancelled_cleanup(
|
||||||
spec: BindspaceSpec = BindspaceSpec(
|
spec: BindspaceSpec = BindspaceSpec(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key='tractor-wg0',
|
key='tractor-wg0',
|
||||||
|
lifecycle='open',
|
||||||
)
|
)
|
||||||
with trio.CancelScope() as scope:
|
with trio.CancelScope() as scope:
|
||||||
async with open_netns(spec):
|
async with open_netns(spec):
|
||||||
|
|
@ -529,6 +547,7 @@ def test_open_netns_requires_name() -> None:
|
||||||
spec: BindspaceSpec = BindspaceSpec(
|
spec: BindspaceSpec = BindspaceSpec(
|
||||||
kind='netns',
|
kind='netns',
|
||||||
key=CURRENT_NETNS,
|
key=CURRENT_NETNS,
|
||||||
|
lifecycle='open',
|
||||||
)
|
)
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,12 @@ from ._bindspace import (
|
||||||
BindspaceHandle as BindspaceHandle,
|
BindspaceHandle as BindspaceHandle,
|
||||||
BindspaceIdentity as BindspaceIdentity,
|
BindspaceIdentity as BindspaceIdentity,
|
||||||
BindspaceKind as BindspaceKind,
|
BindspaceKind as BindspaceKind,
|
||||||
|
BindspaceLifecycle as BindspaceLifecycle,
|
||||||
BindspaceOwnership as BindspaceOwnership,
|
BindspaceOwnership as BindspaceOwnership,
|
||||||
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,
|
||||||
|
open_bindspace as open_bindspace,
|
||||||
open_netns as open_netns,
|
open_netns as open_netns,
|
||||||
)
|
)
|
||||||
from ._multiaddr import (
|
from ._multiaddr import (
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ from ..msg._local import ProcessLocal
|
||||||
BindspaceKind: TypeAlias = Literal[
|
BindspaceKind: TypeAlias = Literal[
|
||||||
'netns',
|
'netns',
|
||||||
]
|
]
|
||||||
|
BindspaceLifecycle: TypeAlias = Literal[
|
||||||
|
'attach', # borrow one existing platform resource
|
||||||
|
'open', # create, own and remove one platform resource
|
||||||
|
]
|
||||||
BindspaceOwnership: TypeAlias = Literal[
|
BindspaceOwnership: TypeAlias = Literal[
|
||||||
'owned', # manager tears down the resource after final release
|
'owned', # manager tears down the resource after final release
|
||||||
'borrowed', # manager leaves the pre-existing resource intact
|
'borrowed', # manager leaves the pre-existing resource intact
|
||||||
|
|
@ -65,6 +69,19 @@ def _validate_bindspace_kind(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_bindspace_lifecycle(
|
||||||
|
lifecycle: BindspaceLifecycle,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Reject lifecycle policies without an implementation.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if lifecycle not in get_args(BindspaceLifecycle):
|
||||||
|
raise ValueError(
|
||||||
|
f'Unsupported bindspace lifecycle: {lifecycle!r}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_bindspace_key(
|
def _validate_bindspace_key(
|
||||||
kind: BindspaceKind,
|
kind: BindspaceKind,
|
||||||
key: str|None,
|
key: str|None,
|
||||||
|
|
@ -112,6 +129,7 @@ class BindspaceSpec(
|
||||||
'''
|
'''
|
||||||
kind: BindspaceKind
|
kind: BindspaceKind
|
||||||
key: str|None = CURRENT_NETNS
|
key: str|None = CURRENT_NETNS
|
||||||
|
lifecycle: BindspaceLifecycle = 'attach'
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -119,6 +137,7 @@ class BindspaceSpec(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
_validate_bindspace_kind(self.kind)
|
_validate_bindspace_kind(self.kind)
|
||||||
|
_validate_bindspace_lifecycle(self.lifecycle)
|
||||||
_validate_bindspace_key(
|
_validate_bindspace_key(
|
||||||
self.kind,
|
self.kind,
|
||||||
self.key,
|
self.key,
|
||||||
|
|
@ -207,6 +226,16 @@ class BindspaceHandle(
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Invalid bindspace ownership: {ownership!r}'
|
f'Invalid bindspace ownership: {ownership!r}'
|
||||||
)
|
)
|
||||||
|
expected_ownership: BindspaceOwnership = (
|
||||||
|
'borrowed'
|
||||||
|
if spec.lifecycle == 'attach'
|
||||||
|
else 'owned'
|
||||||
|
)
|
||||||
|
if ownership != expected_ownership:
|
||||||
|
raise ValueError(
|
||||||
|
f'`BindspaceSpec.lifecycle={spec.lifecycle!r}` '
|
||||||
|
f'requires ownership={expected_ownership!r}!'
|
||||||
|
)
|
||||||
if namespace_fd is not None:
|
if namespace_fd is not None:
|
||||||
if (
|
if (
|
||||||
type(namespace_fd) is not int
|
type(namespace_fd) is not int
|
||||||
|
|
@ -237,23 +266,14 @@ class BindspaceHandle(
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def attach_netns(
|
async def _pin_netns(
|
||||||
spec: BindspaceSpec,
|
spec: BindspaceSpec,
|
||||||
|
ownership: BindspaceOwnership,
|
||||||
) -> AsyncIterator[BindspaceHandle]:
|
) -> AsyncIterator[BindspaceHandle]:
|
||||||
'''
|
'''
|
||||||
Borrow and pin one existing Linux network namespace.
|
Pin one existing Linux network namespace with explicit ownership.
|
||||||
|
|
||||||
`BindspaceSpec.key = CURRENT_NETNS` selects the calling process's
|
|
||||||
current netns. A named key resolves beneath the standard iproute2
|
|
||||||
netns run directory. "Attach" pins an existing namespace FD; this
|
|
||||||
context never calls `setns()` or creates/removes a namespace.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if sys.platform != 'linux':
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Network namespace bindspaces are Linux-only!'
|
|
||||||
)
|
|
||||||
|
|
||||||
key: str|None = spec.key
|
key: str|None = spec.key
|
||||||
namespace_path: Path = (
|
namespace_path: Path = (
|
||||||
_SELF_NETNS
|
_SELF_NETNS
|
||||||
|
|
@ -275,13 +295,41 @@ async def attach_netns(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
identity=identity,
|
identity=identity,
|
||||||
namespace_fd=namespace_fd,
|
namespace_fd=namespace_fd,
|
||||||
ownership='borrowed',
|
ownership=ownership,
|
||||||
)
|
)
|
||||||
yield handle
|
yield handle
|
||||||
finally:
|
finally:
|
||||||
os.close(namespace_fd)
|
os.close(namespace_fd)
|
||||||
|
|
||||||
|
|
||||||
|
@acm
|
||||||
|
async def attach_netns(
|
||||||
|
spec: BindspaceSpec,
|
||||||
|
) -> AsyncIterator[BindspaceHandle]:
|
||||||
|
'''
|
||||||
|
Borrow and pin one existing Linux network namespace.
|
||||||
|
|
||||||
|
`BindspaceSpec.key = CURRENT_NETNS` selects the calling process's
|
||||||
|
current netns. A named key resolves beneath the standard iproute2
|
||||||
|
netns run directory. "Attach" pins an existing namespace FD; this
|
||||||
|
context never calls `setns()` or creates/removes a namespace.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if sys.platform != 'linux':
|
||||||
|
raise NotImplementedError(
|
||||||
|
'Network namespace bindspaces are Linux-only!'
|
||||||
|
)
|
||||||
|
if spec.lifecycle != 'attach':
|
||||||
|
raise ValueError(
|
||||||
|
'`attach_netns()` requires lifecycle=`attach`!'
|
||||||
|
)
|
||||||
|
async with _pin_netns(
|
||||||
|
spec,
|
||||||
|
ownership='borrowed',
|
||||||
|
) as handle:
|
||||||
|
yield handle
|
||||||
|
|
||||||
|
|
||||||
def _create_netns(
|
def _create_netns(
|
||||||
key: str,
|
key: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -332,6 +380,10 @@ async def open_netns(
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
'Network namespace bindspaces are Linux-only!'
|
'Network namespace bindspaces are Linux-only!'
|
||||||
)
|
)
|
||||||
|
if spec.lifecycle != 'open':
|
||||||
|
raise ValueError(
|
||||||
|
'`open_netns()` requires lifecycle=`open`!'
|
||||||
|
)
|
||||||
|
|
||||||
key: str|None = spec.key
|
key: str|None = spec.key
|
||||||
if key is CURRENT_NETNS:
|
if key is CURRENT_NETNS:
|
||||||
|
|
@ -349,13 +401,10 @@ async def open_netns(
|
||||||
)
|
)
|
||||||
created = True
|
created = True
|
||||||
|
|
||||||
async with attach_netns(spec) as borrowed:
|
async with _pin_netns(
|
||||||
handle: BindspaceHandle = BindspaceHandle(
|
spec,
|
||||||
spec=spec,
|
|
||||||
identity=borrowed.identity,
|
|
||||||
namespace_fd=borrowed.namespace_fd,
|
|
||||||
ownership='owned',
|
ownership='owned',
|
||||||
)
|
) as handle:
|
||||||
yield handle
|
yield handle
|
||||||
finally:
|
finally:
|
||||||
if created:
|
if created:
|
||||||
|
|
@ -365,3 +414,22 @@ async def open_netns(
|
||||||
key,
|
key,
|
||||||
abandon_on_cancel=False,
|
abandon_on_cancel=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@acm
|
||||||
|
async def open_bindspace(
|
||||||
|
spec: BindspaceSpec,
|
||||||
|
) -> AsyncIterator[BindspaceHandle]:
|
||||||
|
'''
|
||||||
|
Dispatch one declared bindspace lifecycle.
|
||||||
|
|
||||||
|
Lifecycle is explicit serialized policy. It is never inferred
|
||||||
|
from whether the eventual transport role is listen or dial.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if spec.lifecycle == 'attach':
|
||||||
|
async with attach_netns(spec) as handle:
|
||||||
|
yield handle
|
||||||
|
else:
|
||||||
|
async with open_netns(spec) as handle:
|
||||||
|
yield handle
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue