feat: implement dynamic decky mutation and fix dot-separated INI sections
This commit is contained in:
@@ -54,6 +54,7 @@ class DeckySpec:
|
||||
archetype: str | None = None
|
||||
service_config: dict[str, dict] = field(default_factory=dict)
|
||||
nmap_os: str | None = None # explicit OS family override (linux/windows/bsd/embedded/cisco)
|
||||
mutate_interval: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -71,6 +72,7 @@ class IniConfig:
|
||||
gateway: str | None = None
|
||||
interface: str | None = None
|
||||
log_target: str | None = None
|
||||
mutate_interval: int | None = None
|
||||
deckies: list[DeckySpec] = field(default_factory=list)
|
||||
custom_services: list[CustomServiceSpec] = field(default_factory=list)
|
||||
|
||||
@@ -91,12 +93,23 @@ def load_ini(path: str | Path) -> IniConfig:
|
||||
cfg.interface = g.get("interface")
|
||||
cfg.log_target = g.get("log_target") or g.get("log-target")
|
||||
|
||||
from decnet.services.registry import all_services
|
||||
known_services = set(all_services().keys())
|
||||
|
||||
# First pass: collect decky sections and custom service definitions
|
||||
for section in cp.sections():
|
||||
if section == "general":
|
||||
continue
|
||||
|
||||
# A service sub-section is identified if the section name has at least one dot
|
||||
# AND the last segment is a known service name.
|
||||
# e.g. "decky-01.ssh" -> sub-section
|
||||
# e.g. "decky.webmail" -> decky section (if "webmail" is not a service)
|
||||
if "." in section:
|
||||
continue # subsections handled in second pass
|
||||
_, _, last_segment = section.rpartition(".")
|
||||
if last_segment in known_services:
|
||||
continue # sub-section handled in second pass
|
||||
|
||||
if section.startswith("custom-"):
|
||||
# Bring-your-own service definition
|
||||
s = cp[section]
|
||||
@@ -115,6 +128,15 @@ def load_ini(path: str | Path) -> IniConfig:
|
||||
services = [sv.strip() for sv in svc_raw.split(",")] if svc_raw else None
|
||||
archetype = s.get("archetype")
|
||||
nmap_os = s.get("nmap_os") or s.get("nmap-os") or None
|
||||
|
||||
mi_raw = s.get("mutate_interval") or s.get("mutate-interval")
|
||||
mutate_interval = None
|
||||
if mi_raw:
|
||||
try:
|
||||
mutate_interval = int(mi_raw)
|
||||
except ValueError:
|
||||
raise ValueError(f"[{section}] mutate_interval= must be an integer, got '{mi_raw}'")
|
||||
|
||||
amount_raw = s.get("amount", "1")
|
||||
try:
|
||||
amount = int(amount_raw)
|
||||
@@ -125,7 +147,7 @@ def load_ini(path: str | Path) -> IniConfig:
|
||||
|
||||
if amount == 1:
|
||||
cfg.deckies.append(DeckySpec(
|
||||
name=section, ip=ip, services=services, archetype=archetype, nmap_os=nmap_os,
|
||||
name=section, ip=ip, services=services, archetype=archetype, nmap_os=nmap_os, mutate_interval=mutate_interval,
|
||||
))
|
||||
else:
|
||||
# Expand into N deckies; explicit ip is ignored (can't share one IP)
|
||||
@@ -141,6 +163,7 @@ def load_ini(path: str | Path) -> IniConfig:
|
||||
services=services,
|
||||
archetype=archetype,
|
||||
nmap_os=nmap_os,
|
||||
mutate_interval=mutate_interval,
|
||||
))
|
||||
|
||||
# Second pass: collect per-service subsections [decky-name.service]
|
||||
@@ -149,7 +172,11 @@ def load_ini(path: str | Path) -> IniConfig:
|
||||
for section in cp.sections():
|
||||
if "." not in section:
|
||||
continue
|
||||
decky_name, _, svc_name = section.partition(".")
|
||||
|
||||
decky_name, dot, svc_name = section.rpartition(".")
|
||||
if svc_name not in known_services:
|
||||
continue # not a service sub-section
|
||||
|
||||
svc_cfg = {k: v for k, v in cp[section].items()}
|
||||
if decky_name in decky_map:
|
||||
# Direct match — single decky
|
||||
|
||||
Reference in New Issue
Block a user