From: Thomas Walker Lynch Date: Fri, 12 Sep 2025 15:56:11 +0000 (-0700) Subject: minimal testing, but stage scripts look to be complete X-Git-Url: https://git.reasoningtechnology.com/style/static/git-logo.png?a=commitdiff_plain;h=27d5647d2aa73000857a9350608cf9beae7320b2;p=subu minimal testing, but stage scripts look to be complete --- diff --git a/developer/Python/wg/db_init_client_incommon.py b/developer/Python/wg/db_init_client_incommon.py deleted file mode 100644 index 1f9443e..0000000 --- a/developer/Python/wg/db_init_client_incommon.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -# Helpers to seed/update a row in client. - -from __future__ import annotations -import sqlite3 -from typing import Any, Optional, Dict -import incommon as ic # provides DB_PATH, open_db - -# Normally don't set the addr_cidr, the system will automically -# assign a free address, or reuse one that is already set. - -def upsert_client(conn: sqlite3.Connection, - *, - iface: str, - addr_cidr: Optional[str] = None, - rt_table_name: Optional[str] = None, - rt_table_id: Optional[int] = None, - mtu: Optional[int] = None, - fwmark: Optional[int] = None, - dns_mode: Optional[str] = None, # 'none' or 'static' - dns_servers: Optional[str] = None, - autostart: Optional[int] = None, # 0 or 1 - bound_user: Optional[str] = None, - bound_uid: Optional[int] = None - ) -> str: - row = conn.execute( - """SELECT id, iface, rt_table_id, rt_table_name, local_address_cidr, - mtu, fwmark, dns_mode, dns_servers, autostart, - bound_user, bound_uid - FROM Iface WHERE iface=? LIMIT 1;""", - (iface,) - ).fetchone() - - defname = rt_table_name if rt_table_name is not None else iface - desired: Dict[str, Any] = {"iface": iface, "local_address_cidr": addr_cidr} - if rt_table_id is not None: desired["rt_table_id"] = rt_table_id - if rt_table_name is not None: desired["rt_table_name"] = rt_table_name - if mtu is not None: desired["mtu"] = mtu - if fwmark is not None: desired["fwmark"] = fwmark - if dns_mode is not None: desired["dns_mode"] = dns_mode - if dns_servers is not None: desired["dns_servers"] = dns_servers - if autostart is not None: desired["autostart"] = autostart - if bound_user is not None: desired["bound_user"] = bound_user - if bound_uid is not None: desired["bound_uid"] = bound_uid - - if row is None: - fields = ["iface","local_address_cidr","rt_table_name"] - vals = [iface, addr_cidr, defname] - for k in ("rt_table_id","mtu","fwmark","dns_mode","dns_servers","autostart","bound_user","bound_uid"): - if k in desired: fields.append(k); vals.append(desired[k]) - q = f"INSERT INTO Iface ({','.join(fields)}) VALUES ({','.join('?' for _ in vals)});" - cur = conn.execute(q, vals); conn.commit() - return f"seeded: client(iface={iface}) id={cur.lastrowid} addr={addr_cidr} rt={defname}" - else: - cid, _, rt_id, rt_name, cur_addr, cur_mtu, cur_fwm, cur_dns_mode, cur_dns_srv, cur_auto, cur_buser, cur_buid = row - current = { - "local_address_cidr": cur_addr, "rt_table_id": rt_id, "rt_table_name": rt_name, - "mtu": cur_mtu, "fwmark": cur_fwm, "dns_mode": cur_dns_mode, "dns_servers": cur_dns_srv, - "autostart": cur_auto, "bound_user": cur_buser, "bound_uid": cur_buid - } - changes: Dict[str, Any] = {} - for k, v in desired.items(): - if k == "iface": continue - if current.get(k) != v: changes[k] = v - if rt_name is None and "rt_table_name" not in changes: - changes["rt_table_name"] = defname - if not changes: - return f"ok: client(iface={iface}) unchanged id={cid} addr={cur_addr} rt={rt_name or defname}" - sets = ", ".join(f"{k}=?" for k in changes) - vals = list(changes.values()) + [iface] - conn.execute(f"UPDATE Iface SET {sets} WHERE iface=?;", vals); conn.commit() - return f"updated: client(iface={iface}) id={cid} " + " ".join(f"{k}={changes[k]}" for k in changes) diff --git a/developer/Python/wg/db_init_iface.py b/developer/Python/wg/db_init_iface.py new file mode 100644 index 0000000..1f9443e --- /dev/null +++ b/developer/Python/wg/db_init_iface.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# Helpers to seed/update a row in client. + +from __future__ import annotations +import sqlite3 +from typing import Any, Optional, Dict +import incommon as ic # provides DB_PATH, open_db + +# Normally don't set the addr_cidr, the system will automically +# assign a free address, or reuse one that is already set. + +def upsert_client(conn: sqlite3.Connection, + *, + iface: str, + addr_cidr: Optional[str] = None, + rt_table_name: Optional[str] = None, + rt_table_id: Optional[int] = None, + mtu: Optional[int] = None, + fwmark: Optional[int] = None, + dns_mode: Optional[str] = None, # 'none' or 'static' + dns_servers: Optional[str] = None, + autostart: Optional[int] = None, # 0 or 1 + bound_user: Optional[str] = None, + bound_uid: Optional[int] = None + ) -> str: + row = conn.execute( + """SELECT id, iface, rt_table_id, rt_table_name, local_address_cidr, + mtu, fwmark, dns_mode, dns_servers, autostart, + bound_user, bound_uid + FROM Iface WHERE iface=? LIMIT 1;""", + (iface,) + ).fetchone() + + defname = rt_table_name if rt_table_name is not None else iface + desired: Dict[str, Any] = {"iface": iface, "local_address_cidr": addr_cidr} + if rt_table_id is not None: desired["rt_table_id"] = rt_table_id + if rt_table_name is not None: desired["rt_table_name"] = rt_table_name + if mtu is not None: desired["mtu"] = mtu + if fwmark is not None: desired["fwmark"] = fwmark + if dns_mode is not None: desired["dns_mode"] = dns_mode + if dns_servers is not None: desired["dns_servers"] = dns_servers + if autostart is not None: desired["autostart"] = autostart + if bound_user is not None: desired["bound_user"] = bound_user + if bound_uid is not None: desired["bound_uid"] = bound_uid + + if row is None: + fields = ["iface","local_address_cidr","rt_table_name"] + vals = [iface, addr_cidr, defname] + for k in ("rt_table_id","mtu","fwmark","dns_mode","dns_servers","autostart","bound_user","bound_uid"): + if k in desired: fields.append(k); vals.append(desired[k]) + q = f"INSERT INTO Iface ({','.join(fields)}) VALUES ({','.join('?' for _ in vals)});" + cur = conn.execute(q, vals); conn.commit() + return f"seeded: client(iface={iface}) id={cur.lastrowid} addr={addr_cidr} rt={defname}" + else: + cid, _, rt_id, rt_name, cur_addr, cur_mtu, cur_fwm, cur_dns_mode, cur_dns_srv, cur_auto, cur_buser, cur_buid = row + current = { + "local_address_cidr": cur_addr, "rt_table_id": rt_id, "rt_table_name": rt_name, + "mtu": cur_mtu, "fwmark": cur_fwm, "dns_mode": cur_dns_mode, "dns_servers": cur_dns_srv, + "autostart": cur_auto, "bound_user": cur_buser, "bound_uid": cur_buid + } + changes: Dict[str, Any] = {} + for k, v in desired.items(): + if k == "iface": continue + if current.get(k) != v: changes[k] = v + if rt_name is None and "rt_table_name" not in changes: + changes["rt_table_name"] = defname + if not changes: + return f"ok: client(iface={iface}) unchanged id={cid} addr={cur_addr} rt={rt_name or defname}" + sets = ", ".join(f"{k}=?" for k in changes) + vals = list(changes.values()) + [iface] + conn.execute(f"UPDATE Iface SET {sets} WHERE iface=?;", vals); conn.commit() + return f"updated: client(iface={iface}) id={cid} " + " ".join(f"{k}={changes[k]}" for k in changes) diff --git a/developer/Python/wg/db_init_iface_US.py b/developer/Python/wg/db_init_iface_US.py index 45d0447..bf03c95 100755 --- a/developer/Python/wg/db_init_iface_US.py +++ b/developer/Python/wg/db_init_iface_US.py @@ -1,5 +1,5 @@ # db_init_iface_US.py -from db_init_client_incommon import upsert_client +from db_init_iface import upsert_client def init_iface_US(conn): # iface US with dedicated table 'US' and a distinct host /32 diff --git a/developer/Python/wg/db_init_iface_x6.py b/developer/Python/wg/db_init_iface_x6.py index 38794c7..82eb5fe 100755 --- a/developer/Python/wg/db_init_iface_x6.py +++ b/developer/Python/wg/db_init_iface_x6.py @@ -1,5 +1,5 @@ # db_init_iface_x6.py -from db_init_client_incommon import upsert_client +from db_init_iface import upsert_client def init_iface_x6(conn): # iface x6 with dedicated table 'x6' and host /32 diff --git a/developer/Python/wg/doc_config.org b/developer/Python/wg/doc_config.org new file mode 100644 index 0000000..2de0ee4 --- /dev/null +++ b/developer/Python/wg/doc_config.org @@ -0,0 +1,9 @@ +-New interface: + +copy `db_init_iface_x6.py` to `db_init_iface_.py`, replacing with the name of the interface. Then edit `db_init_iface_.py` + +-New Client + +-New User + + diff --git a/developer/Python/wg/stage_StanleyPark.py b/developer/Python/wg/stage_StanleyPark.py new file mode 100644 index 0000000..77264a3 --- /dev/null +++ b/developer/Python/wg/stage_StanleyPark.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +""" +stage_StanleyPark.py + +Minimal config wrapper for client 'StanleyPark'. +Calls the generic stage orchestrator with the chosen ifaces. +""" + +from __future__ import annotations +from stage_client import stage_client_artifacts + +CLIENT = "StanleyPark" +IFACES = ["x6","US"] # keep this list minimal & declarative + +if __name__ == "__main__": + ok = stage_client_artifacts( + CLIENT + ,IFACES + ) + raise SystemExit(0 if ok else 2) diff --git a/developer/Python/wg/stage_client.py b/developer/Python/wg/stage_client.py new file mode 100644 index 0000000..7d5f5ba --- /dev/null +++ b/developer/Python/wg/stage_client.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +""" +stage_client.py + +Given: + - A SQLite DB reachable via incommon.open_db() + - A client machine name (used to locate ./key/ for WG PrivateKey) + - One or more interface names (e.g., x6, US) + +Does: + 1) Stage WireGuard confs for each iface (Table=off; ListenPort commented if NULL) + 2) Stage /etc/iproute2/rt_tables entries for those ifaces + 3) Stage a unified IP apply script (addresses, routes, rules) + 4) Stage per-iface systemd drop-ins to invoke the apply script on wg-quick up + +Returns: + - True on success, False on failure + - Prints human-readable progress for each step + +Errors: + - Raises or prints clear ❌ messages on failure +""" + +from __future__ import annotations +from pathlib import Path +from typing import Callable ,Optional ,Sequence ,Tuple +import argparse +import subprocess +import sys + +import incommon as ic # open_db() + +ROOT = Path(__file__).resolve().parent +STAGE_ROOT = ROOT / "stage" + + +def _msg_wrapped_call(label: str ,fn: Callable[[], Tuple[Path ,Sequence[str]]]) -> bool: + print(f"→ {label}") + try: + path ,notes = fn() + for n in notes: + print(n) + if path: + print(f"✔ {label}: staged: {path}") + else: + print(f"✔ {label}") + return True + except Exception as e: + print(f"❌ {label}: {e}") + return False + + +def _call_cli(argv: Sequence[str]) -> Tuple[Path ,Sequence[str]]: + cp = subprocess.run(list(argv) ,text=True ,capture_output=True) + if cp.returncode != 0: + raise RuntimeError(cp.stderr.strip() or f"exit {cp.returncode}") + notes = [] + staged_path: Optional[Path] = None + for line in (cp.stdout or "").splitlines(): + notes.append(line) + if line.startswith("staged: "): + try: + staged_path = Path(line.split("staged:",1)[1].strip()) + except Exception: + pass + return (staged_path or STAGE_ROOT ,notes) + + +def _stage_wg_conf_step(client_name: str ,ifaces: Sequence[str]) -> bool: + def _do(): + try: + from stage_wg_conf import stage_wg_conf # type: ignore + with ic.open_db() as conn: + path ,notes = stage_wg_conf( + conn + ,ifaces + ,client_name + ,stage_root=STAGE_ROOT + ,dry_run=False + ) + return (path ,notes) + except Exception: + return _call_cli([str(ROOT / "stage_wg_conf.py") ,client_name ,*ifaces]) + return _msg_wrapped_call(f"stage_wg_conf ({client_name}; {','.join(ifaces)})" ,_do) + + +def _stage_rt_tables_step(ifaces: Sequence[str]) -> bool: + def _do(): + try: + from stage_IP_register_route_table import stage_ip_register_route_table # type: ignore + with ic.open_db() as conn: + path ,notes = stage_ip_register_route_table( + conn + ,ifaces + ,stage_root=STAGE_ROOT + ,dry_run=False + ) + return (path ,notes) + except Exception: + return _call_cli([str(ROOT / "stage_IP_register_route_table.py") ,*ifaces]) + return _msg_wrapped_call(f"stage_IP_register_route_table ({','.join(ifaces)})" ,_do) + + +def _stage_apply_ip_state_step(ifaces: Sequence[str]) -> bool: + def _do(): + try: + from stage_IP_apply_script import stage_ip_apply_script # type: ignore + with ic.open_db() as conn: + path ,notes = stage_ip_apply_script( + conn + ,ifaces + ,stage_root=STAGE_ROOT + ,script_name="apply_ip_state.sh" + ,only_on_up=True + ,dry_run=False + ) + return (path ,notes) + except Exception: + return _call_cli([str(ROOT / "stage_IP_apply_script.py") ,*ifaces]) + return _msg_wrapped_call(f"stage_IP_apply_script ({','.join(ifaces)})" ,_do) + + +def stage_client_artifacts( + client_name: str + ,iface_names: Sequence[str] + ,stage_root: Optional[Path] = None +) -> bool: + """ + Orchestrate staging for a client+ifaces. Prints progress and returns success. + """ + if not iface_names: + raise ValueError("no interfaces provided") + if stage_root: + global STAGE_ROOT + STAGE_ROOT = stage_root + + STAGE_ROOT.mkdir(parents=True ,exist_ok=True) + + ok = True + ok = _stage_wg_conf_step(client_name ,iface_names) and ok + ok = _stage_rt_tables_step(iface_names) and ok + ok = _stage_apply_ip_state_step(iface_names) and ok + return ok + + +def main(argv: Optional[Sequence[str]] = None) -> int: + ap = argparse.ArgumentParser(description="Stage all artifacts for a client.") + ap.add_argument("--client" ,required=True ,help="client machine name (for key lookup)") + ap.add_argument("ifaces" ,nargs="+") + args = ap.parse_args(argv) + + ok = stage_client_artifacts( + args.client + ,args.ifaces + ) + return 0 if ok else 2 + + +if __name__ == "__main__": + sys.exit(main())