Files
apex.ermnvldmr.com/engine/identity.py
T
deytenit bd973d783d refactor: make engine domain-agnostic — identity from node.env, no hardcoded domain
Drop the hardcoded-domain FQDN regex and every hardcoded domain from commons.
Node identity is now explicit in node.env: APEX_NODE_FQDN (real FQDN,
traefik Host()), APEX_NODE_HOST (public host name — backup --host,
obs/notify instance), APEX_SUBNET. The short name (log prefix, sync/<node>
branch, messages) is derived as the first host label.

notify instance is just the host (no hardcoded prefix); the git bot identity
moves to APEX_GIT_AUTHOR_NAME/EMAIL (node.env) with generic defaults. Alloy
node+instance labels use APEX_NODE_HOST. Fallbacks keep a node working if it
omits a var (host<-fqdn<-hostname).

BREAKING CHANGE: removes APEX_NODE and APEX_CLUSTER env vars; node name is now derived from APEX_NODE_FQDN
2026-07-08 09:32:20 +03:00

65 lines
2.6 KiB
Python

# engine/identity.py
"""Node identity — fully explicit, from node.env. The framework assumes no domain
and no clustering: a node declares its real FQDN and its public host name, and the
short `name` is just the first label. The OS hostname is only a fallback for the FQDN.
node.env keys:
APEX_NODE_FQDN real FQDN (traefik Host() routing). default: OS hostname
APEX_NODE_HOST public host name (backup --host, obs/notify default: APEX_NODE_FQDN
`instance`); often the FQDN minus a cluster
segment, but that's the node's choice.
APEX_SUBNET the node's /24 for the `direct` network. required
"""
from __future__ import annotations
import os
import sys
from dataclasses import dataclass
from typing import Dict, List, Tuple
@dataclass
class Node:
name: str # short label (first segment of the host), e.g. "icarus" — logs, branch, messages
subnet: str # the node's direct-network /24
fqdn: str # real FQDN — traefik Host() routing
host: str # public host name — backup --host, obs `instance`, notify instance
def read_env(path: str) -> Dict[str, str]:
"""Parse a KEY=value file (shell-sourceable). Ignores comments/blank lines."""
out: Dict[str, str] = {}
if not os.path.isfile(path):
return out
with open(path) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
out[key.strip()] = val.strip().strip('"').strip("'")
return out
def resolve(hostname: str, repo_root: str) -> Tuple[Node, List[str]]:
"""Return (Node, warnings). Exits 66 (EX_NOINPUT) if APEX_SUBNET is missing."""
warns: List[str] = []
env = read_env(os.path.join(repo_root, "node.env"))
fqdn = env.get("APEX_NODE_FQDN", "") or (hostname or "").strip()
if not fqdn:
sys.stderr.write("identity: APEX_NODE_FQDN missing from node.env and no hostname\n")
raise SystemExit(66)
if not env.get("APEX_NODE_FQDN"):
warns.append(f"APEX_NODE_FQDN not set in node.env; using hostname {fqdn!r}.")
host = env.get("APEX_NODE_HOST", "") or fqdn # node's public name; defaults to the FQDN
name = host.split(".")[0] # short label for logs/branch/messages
subnet = env.get("APEX_SUBNET", "")
if not subnet:
sys.stderr.write("identity: APEX_SUBNET missing from node.env\n")
raise SystemExit(66)
return Node(name=name, subnet=subnet, fqdn=fqdn, host=host), warns