Phase 1 abgeschlossen: Node-Identitaet in Control Core verkabelt (§3.6, §6.3)
- NodeIdentity/NodeRole in hms_domain: persistente node_id aus userdata/identity, Rollen RENDER_NODE/COORDINATOR/CONTROL_DESK, plausible Kombinationen validiert, keine Auto-Leader-Wahl (§6.3) - Control Core: create_app(identity) injizierbar; Dev-Modus ephemeral; Registry registriert eigene Node; neue Endpunkte /system/identity (ohne Secrets, §27.1) und /cluster/nodes (UI-Kategorien §6.3) - 16 neue/aktualisierte Integrationstests; Gesamtsuite 199 gruen
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
"""hms_domain – plattformneutrale Domänenobjekte (PLAN.md §10)."""
|
||||
|
||||
from hms_domain.identity import NodeIdentity, NodeRole
|
||||
from hms_domain.ids import new_node_id, new_uuid, persistent_node_id
|
||||
|
||||
__all__ = ["new_uuid", "new_node_id", "persistent_node_id"]
|
||||
__all__ = [
|
||||
"new_uuid",
|
||||
"new_node_id",
|
||||
"persistent_node_id",
|
||||
"NodeIdentity",
|
||||
"NodeRole",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Node-Identität und Rollen (PLAN.md §6.3, §10.1, §3.6).
|
||||
|
||||
Der Control Core lädt beim Start:
|
||||
- persistente node_id aus userdata/identity/node_id (IP-/hostname-unabhängig)
|
||||
- konfigurierbare Rollen (RENDER_NODE, COORDINATOR, CONTROL_DESK)
|
||||
- Anzeigename (editierbar, nicht identitätsstiftend)
|
||||
|
||||
Rollen sind Pflicht für Discovery (TXT 'roles') und Cluster-Protokoll.
|
||||
Es gibt in V1 keine automatische Leader-Wahl (§6.3): der Betreiber legt die
|
||||
Coordinator-Rolle fest.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
|
||||
from hms_domain.ids import new_node_id, persistent_node_id
|
||||
|
||||
|
||||
class NodeRole(StrEnum):
|
||||
"""Rollen gemäß §6.3. Keine Auto-Leader-Wahl: Rollen sind Konfiguration."""
|
||||
|
||||
RENDER_NODE = "RENDER_NODE"
|
||||
COORDINATOR = "COORDINATOR"
|
||||
CONTROL_DESK = "CONTROL_DESK"
|
||||
|
||||
|
||||
_VALID_COMBOS = {
|
||||
# reine Render-Nodes: decodieren und rendern lokal (§6.3)
|
||||
frozenset({NodeRole.RENDER_NODE}),
|
||||
# Coordinator kann auf einem Render-Node mitlaufen (§6.3)
|
||||
frozenset({NodeRole.RENDER_NODE, NodeRole.COORDINATOR}),
|
||||
# reiner Coordinator / Control-PC ohne Renderer (HEADLESS_CONTROL, §6.3)
|
||||
frozenset({NodeRole.COORDINATOR}),
|
||||
frozenset({NodeRole.CONTROL_DESK}),
|
||||
frozenset({NodeRole.CONTROL_DESK, NodeRole.COORDINATOR}),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NodeIdentity:
|
||||
"""Stabiles Identitätsbündel eines Servers (§3.6, §10.1).
|
||||
|
||||
- node_id: persistent, UUID, unabhängig von IP/Hostname
|
||||
- display_name: editierbar; Umbenennung zerstört keine Automationen,
|
||||
weil Parameterpfade UUID-basiert sind (§10.2)
|
||||
- roles: konfigurierte Rollen; RENDER_NODE impliziert lokalen Renderer
|
||||
"""
|
||||
|
||||
node_id: str
|
||||
display_name: str
|
||||
roles: frozenset[NodeRole]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.node_id:
|
||||
raise ValueError("node_id darf nicht leer sein")
|
||||
if not self.roles:
|
||||
raise ValueError("mindestens eine Rolle erforderlich (§6.3)")
|
||||
if frozenset(self.roles) not in _VALID_COMBOS:
|
||||
allowed = ", ".join(
|
||||
sorted("|".join(sorted(r.value for r in c)) for c in _VALID_COMBOS)
|
||||
)
|
||||
raise ValueError(
|
||||
f"ungültige Rollenkombination {sorted(r.value for r in self.roles)}; "
|
||||
f"erlaubt: {allowed}"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_coordinator(self) -> bool:
|
||||
return NodeRole.COORDINATOR in self.roles
|
||||
|
||||
@property
|
||||
def renders_locally(self) -> bool:
|
||||
return NodeRole.RENDER_NODE in self.roles
|
||||
|
||||
@classmethod
|
||||
def load_or_create(
|
||||
cls,
|
||||
identity_dir: Path,
|
||||
display_name: str,
|
||||
roles: frozenset[NodeRole],
|
||||
) -> NodeIdentity:
|
||||
"""Lädt die persistente node_id oder erzeugt sie genau einmal.
|
||||
|
||||
IP-Wechsel ändern die node_id nicht; Doppelvergabe wird über die
|
||||
Datei (O_EXCL) verhindert (§6.3, hms_domain.ids).
|
||||
"""
|
||||
identity_file = identity_dir / "node_id"
|
||||
node_id = persistent_node_id(identity_file)
|
||||
return cls(node_id=node_id, display_name=display_name, roles=roles)
|
||||
|
||||
@classmethod
|
||||
def ephemeral(cls, display_name: str, roles: frozenset[NodeRole]) -> NodeIdentity:
|
||||
"""Nur für Tests: Identität ohne Persistenz (kein Showbetrieb)."""
|
||||
return cls(node_id=new_node_id(), display_name=display_name, roles=roles)
|
||||
Reference in New Issue
Block a user