|
|
|
@@ -36,7 +36,9 @@ REBUILD_LOG = "/var/log/sovran-hub-rebuild.log"
|
|
|
|
REBUILD_STATUS = "/var/log/sovran-hub-rebuild.status"
|
|
|
|
REBUILD_STATUS = "/var/log/sovran-hub-rebuild.status"
|
|
|
|
REBUILD_UNIT = "sovran-hub-rebuild.service"
|
|
|
|
REBUILD_UNIT = "sovran-hub-rebuild.service"
|
|
|
|
|
|
|
|
|
|
|
|
HUB_OVERRIDES_NIX = "/etc/nixos/hub-overrides.nix"
|
|
|
|
CUSTOM_NIX = "/etc/nixos/custom.nix"
|
|
|
|
|
|
|
|
HUB_BEGIN = " # ── Hub Managed (do not edit) ──────────────"
|
|
|
|
|
|
|
|
HUB_END = " # ── End Hub Managed ────────────────────────"
|
|
|
|
DOMAINS_DIR = "/var/lib/domains"
|
|
|
|
DOMAINS_DIR = "/var/lib/domains"
|
|
|
|
NOSTR_NPUB_FILE = "/var/lib/secrets/nostr_npub"
|
|
|
|
NOSTR_NPUB_FILE = "/var/lib/secrets/nostr_npub"
|
|
|
|
NJALLA_SCRIPT = "/var/lib/njalla/njalla.sh"
|
|
|
|
NJALLA_SCRIPT = "/var/lib/njalla/njalla.sh"
|
|
|
|
@@ -434,21 +436,30 @@ def _read_rebuild_log(offset: int = 0) -> tuple[str, int]:
|
|
|
|
return "", 0
|
|
|
|
return "", 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── hub-overrides.nix helpers ─────────────────────────────────────
|
|
|
|
# ── custom.nix Hub Managed section helpers ────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def _read_hub_overrides() -> tuple[dict, str | None]:
|
|
|
|
def _read_hub_overrides() -> tuple[dict, str | None]:
|
|
|
|
"""Parse hub-overrides.nix. Returns (features_dict, nostr_npub_or_none)."""
|
|
|
|
"""Parse the Hub Managed section inside custom.nix.
|
|
|
|
|
|
|
|
Returns (features_dict, nostr_npub_or_none)."""
|
|
|
|
features: dict[str, bool] = {}
|
|
|
|
features: dict[str, bool] = {}
|
|
|
|
nostr_npub = None
|
|
|
|
nostr_npub = None
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
with open(HUB_OVERRIDES_NIX, "r") as f:
|
|
|
|
with open(CUSTOM_NIX, "r") as f:
|
|
|
|
content = f.read()
|
|
|
|
content = f.read()
|
|
|
|
|
|
|
|
begin = content.find(HUB_BEGIN)
|
|
|
|
|
|
|
|
end = content.find(HUB_END)
|
|
|
|
|
|
|
|
if begin == -1 or end == -1:
|
|
|
|
|
|
|
|
return features, nostr_npub
|
|
|
|
|
|
|
|
section = content[begin:end]
|
|
|
|
for m in re.finditer(
|
|
|
|
for m in re.finditer(
|
|
|
|
r'sovran_systemsOS\.features\.([a-zA-Z0-9_-]+)\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
|
|
|
|
r'sovran_systemsOS\.features\.([a-zA-Z0-9_-]+)\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
|
|
|
|
content,
|
|
|
|
section,
|
|
|
|
):
|
|
|
|
):
|
|
|
|
features[m.group(1)] = m.group(2) == "true"
|
|
|
|
features[m.group(1)] = m.group(2) == "true"
|
|
|
|
m2 = re.search(r'sovran_systemsOS\.nostr_npub\s*=\s*(?:lib\.mkForce\s+)?"([^"]*)"', content)
|
|
|
|
m2 = re.search(
|
|
|
|
|
|
|
|
r'sovran_systemsOS\.nostr_npub\s*=\s*(?:lib\.mkForce\s+)?"([^"]*)"',
|
|
|
|
|
|
|
|
section,
|
|
|
|
|
|
|
|
)
|
|
|
|
if m2:
|
|
|
|
if m2:
|
|
|
|
nostr_npub = m2.group(1)
|
|
|
|
nostr_npub = m2.group(1)
|
|
|
|
except FileNotFoundError:
|
|
|
|
except FileNotFoundError:
|
|
|
|
@@ -457,25 +468,44 @@ def _read_hub_overrides() -> tuple[dict, str | None]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write_hub_overrides(features: dict, nostr_npub: str | None) -> None:
|
|
|
|
def _write_hub_overrides(features: dict, nostr_npub: str | None) -> None:
|
|
|
|
"""Write a complete hub-overrides.nix from the given state."""
|
|
|
|
"""Write the Hub Managed section inside custom.nix."""
|
|
|
|
lines = []
|
|
|
|
lines = []
|
|
|
|
for feat_id, enabled in features.items():
|
|
|
|
for feat_id, enabled in features.items():
|
|
|
|
val = "true" if enabled else "false"
|
|
|
|
val = "true" if enabled else "false"
|
|
|
|
lines.append(f" sovran_systemsOS.features.{feat_id} = lib.mkForce {val};")
|
|
|
|
lines.append(f" sovran_systemsOS.features.{feat_id} = lib.mkForce {val};")
|
|
|
|
if nostr_npub:
|
|
|
|
if nostr_npub:
|
|
|
|
lines.append(f' sovran_systemsOS.nostr_npub = lib.mkForce "{nostr_npub}";')
|
|
|
|
lines.append(f' sovran_systemsOS.nostr_npub = lib.mkForce "{nostr_npub}";')
|
|
|
|
body = "\n".join(lines) + "\n" if lines else ""
|
|
|
|
hub_block = (
|
|
|
|
content = (
|
|
|
|
HUB_BEGIN + "\n"
|
|
|
|
"# Auto-generated by Sovran Hub — do not edit manually\n"
|
|
|
|
+ "\n".join(lines) + ("\n" if lines else "")
|
|
|
|
"{ lib, ... }:\n"
|
|
|
|
+ HUB_END + "\n"
|
|
|
|
"{\n"
|
|
|
|
|
|
|
|
+ body
|
|
|
|
|
|
|
|
+ "}\n"
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
nix_dir = os.path.dirname(HUB_OVERRIDES_NIX)
|
|
|
|
|
|
|
|
if nix_dir:
|
|
|
|
try:
|
|
|
|
os.makedirs(nix_dir, exist_ok=True)
|
|
|
|
with open(CUSTOM_NIX, "r") as f:
|
|
|
|
with open(HUB_OVERRIDES_NIX, "w") as f:
|
|
|
|
content = f.read()
|
|
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
begin = content.find(HUB_BEGIN)
|
|
|
|
|
|
|
|
end = content.find(HUB_END)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if begin != -1 and end != -1:
|
|
|
|
|
|
|
|
# Replace existing hub section (include the HUB_END line itself)
|
|
|
|
|
|
|
|
newline_after_end = content.find("\n", end)
|
|
|
|
|
|
|
|
if newline_after_end == -1:
|
|
|
|
|
|
|
|
end_of_marker = len(content)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
end_of_marker = newline_after_end + 1
|
|
|
|
|
|
|
|
content = content[:begin] + hub_block + content[end_of_marker:]
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
# Insert hub section just before the final closing }
|
|
|
|
|
|
|
|
last_brace = content.rfind("}")
|
|
|
|
|
|
|
|
if last_brace == -1:
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
content = content[:last_brace] + "\n" + hub_block + content[last_brace:]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with open(CUSTOM_NIX, "w") as f:
|
|
|
|
f.write(content)
|
|
|
|
f.write(content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -631,7 +661,7 @@ async def api_services():
|
|
|
|
|
|
|
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
|
|
|
|
|
|
|
# Read runtime feature overrides from hub-overrides.nix
|
|
|
|
# Read runtime feature overrides from custom.nix Hub Managed section
|
|
|
|
overrides, _ = await loop.run_in_executor(None, _read_hub_overrides)
|
|
|
|
overrides, _ = await loop.run_in_executor(None, _read_hub_overrides)
|
|
|
|
|
|
|
|
|
|
|
|
async def get_status(entry):
|
|
|
|
async def get_status(entry):
|
|
|
|
@@ -640,7 +670,7 @@ async def api_services():
|
|
|
|
icon = entry.get("icon", "")
|
|
|
|
icon = entry.get("icon", "")
|
|
|
|
enabled = entry.get("enabled", True)
|
|
|
|
enabled = entry.get("enabled", True)
|
|
|
|
|
|
|
|
|
|
|
|
# Overlay runtime feature state from hub-overrides.nix
|
|
|
|
# Overlay runtime feature state from custom.nix Hub Managed section
|
|
|
|
feat_id = unit_to_feature.get(unit)
|
|
|
|
feat_id = unit_to_feature.get(unit)
|
|
|
|
if feat_id is None:
|
|
|
|
if feat_id is None:
|
|
|
|
feat_id = FEATURE_ICON_MAP.get(icon)
|
|
|
|
feat_id = FEATURE_ICON_MAP.get(icon)
|
|
|
|
@@ -838,7 +868,7 @@ async def api_features():
|
|
|
|
feat_id = feat["id"]
|
|
|
|
feat_id = feat["id"]
|
|
|
|
|
|
|
|
|
|
|
|
# Determine enabled state:
|
|
|
|
# Determine enabled state:
|
|
|
|
# 1. Check hub-overrides.nix first (explicit hub toggle)
|
|
|
|
# 1. Check custom.nix Hub Managed section first (explicit hub toggle)
|
|
|
|
# 2. Fall back to config.json services (features enabled in custom.nix)
|
|
|
|
# 2. Fall back to config.json services (features enabled in custom.nix)
|
|
|
|
if feat_id in overrides:
|
|
|
|
if feat_id in overrides:
|
|
|
|
enabled = overrides[feat_id]
|
|
|
|
enabled = overrides[feat_id]
|
|
|
|
|