feat(swarm): DeckyConfig.host_uuid + fix agent log/status field refs
- decnet.models.DeckyConfig grows an optional 'host_uuid' (the SwarmHost that runs this decky). Defaults to None so legacy unihost state files deserialize unchanged. - decnet.agent.executor: replace non-existent config.name references with config.mode / config.interface in logs and status payload. - tests/swarm/test_state_schema.py covers legacy-dict roundtrip, field default, and swarm-mode assignments.
This commit is contained in:
@@ -21,7 +21,10 @@ log = get_logger("agent.executor")
|
|||||||
async def deploy(config: DecnetConfig, dry_run: bool = False, no_cache: bool = False) -> None:
|
async def deploy(config: DecnetConfig, dry_run: bool = False, no_cache: bool = False) -> None:
|
||||||
"""Run the blocking deployer off-loop. The deployer itself calls
|
"""Run the blocking deployer off-loop. The deployer itself calls
|
||||||
save_state() internally once the compose file is materialised."""
|
save_state() internally once the compose file is materialised."""
|
||||||
log.info("agent.deploy name=%s deckies=%d", config.name, len(config.deckies))
|
log.info(
|
||||||
|
"agent.deploy mode=%s deckies=%d interface=%s",
|
||||||
|
config.mode, len(config.deckies), config.interface,
|
||||||
|
)
|
||||||
await asyncio.to_thread(_deployer.deploy, config, dry_run, no_cache, False)
|
await asyncio.to_thread(_deployer.deploy, config, dry_run, no_cache, False)
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +42,7 @@ async def status() -> dict[str, Any]:
|
|||||||
config, _compose_path = state
|
config, _compose_path = state
|
||||||
return {
|
return {
|
||||||
"deployed": True,
|
"deployed": True,
|
||||||
"name": getattr(config, "name", None),
|
"mode": config.mode,
|
||||||
"compose_path": str(_compose_path),
|
"compose_path": str(_compose_path),
|
||||||
"deckies": [d.model_dump() for d in config.deckies],
|
"deckies": [d.model_dump() for d in config.deckies],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ class DeckyConfig(BaseModel):
|
|||||||
mutate_interval: int | None = None # automatic rotation interval in minutes
|
mutate_interval: int | None = None # automatic rotation interval in minutes
|
||||||
last_mutated: float = 0.0 # timestamp of last mutation
|
last_mutated: float = 0.0 # timestamp of last mutation
|
||||||
last_login_attempt: float = 0.0 # timestamp of most recent interaction
|
last_login_attempt: float = 0.0 # timestamp of most recent interaction
|
||||||
|
# SWARM: the SwarmHost.uuid that runs this decky. None in unihost mode
|
||||||
|
# so existing state files deserialize unchanged.
|
||||||
|
host_uuid: str | None = None
|
||||||
|
|
||||||
@field_validator("services")
|
@field_validator("services")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
60
tests/swarm/test_state_schema.py
Normal file
60
tests/swarm/test_state_schema.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""Backward-compatibility tests for the SWARM state-schema extension.
|
||||||
|
|
||||||
|
DeckyConfig gained an optional ``host_uuid`` field in swarm mode. Existing
|
||||||
|
state files (unihost) must continue to deserialize without change.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decnet.models import DeckyConfig, DecnetConfig
|
||||||
|
|
||||||
|
|
||||||
|
def _minimal_decky(name: str = "decky-01") -> dict:
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"ip": "192.168.1.10",
|
||||||
|
"services": ["ssh"],
|
||||||
|
"distro": "debian",
|
||||||
|
"base_image": "debian:bookworm-slim",
|
||||||
|
"hostname": "decky01",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_decky_config_host_uuid_defaults_to_none() -> None:
|
||||||
|
"""A decky built from a pre-swarm state blob lands with host_uuid=None."""
|
||||||
|
d = DeckyConfig(**_minimal_decky())
|
||||||
|
assert d.host_uuid is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_decky_config_accepts_host_uuid() -> None:
|
||||||
|
d = DeckyConfig(**_minimal_decky(), host_uuid="host-uuid-abc")
|
||||||
|
assert d.host_uuid == "host-uuid-abc"
|
||||||
|
|
||||||
|
|
||||||
|
def test_decnet_config_mode_swarm_with_host_assignments() -> None:
|
||||||
|
"""Full swarm-mode config: every decky carries a host_uuid."""
|
||||||
|
config = DecnetConfig(
|
||||||
|
mode="swarm",
|
||||||
|
interface="eth0",
|
||||||
|
subnet="192.168.1.0/24",
|
||||||
|
gateway="192.168.1.1",
|
||||||
|
deckies=[
|
||||||
|
DeckyConfig(**_minimal_decky("decky-01"), host_uuid="host-A"),
|
||||||
|
DeckyConfig(**_minimal_decky("decky-02"), host_uuid="host-B"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert config.mode == "swarm"
|
||||||
|
assert {d.host_uuid for d in config.deckies} == {"host-A", "host-B"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_unihost_state_still_parses() -> None:
|
||||||
|
"""A dict matching the pre-swarm schema deserializes unchanged."""
|
||||||
|
legacy_blob = {
|
||||||
|
"mode": "unihost",
|
||||||
|
"interface": "eth0",
|
||||||
|
"subnet": "192.168.1.0/24",
|
||||||
|
"gateway": "192.168.1.1",
|
||||||
|
"deckies": [_minimal_decky()],
|
||||||
|
}
|
||||||
|
config = DecnetConfig.model_validate(legacy_blob)
|
||||||
|
assert config.mode == "unihost"
|
||||||
|
assert config.deckies[0].host_uuid is None
|
||||||
Reference in New Issue
Block a user