From 27d5647d2aa73000857a9350608cf9beae7320b2 Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Fri, 12 Sep 2025 08:56:11 -0700 Subject: [PATCH] minimal testing, but stage scripts look to be complete --- ...it_client_incommon.py => db_init_iface.py} | 0 developer/Python/wg/db_init_iface_US.py | 2 +- developer/Python/wg/db_init_iface_x6.py | 2 +- developer/Python/wg/doc_config.org | 9 + developer/Python/wg/stage_StanleyPark.py | 20 +++ developer/Python/wg/stage_client.py | 160 ++++++++++++++++++ 6 files changed, 191 insertions(+), 2 deletions(-) rename developer/Python/wg/{db_init_client_incommon.py => db_init_iface.py} (100%) create mode 100644 developer/Python/wg/doc_config.org create mode 100644 developer/Python/wg/stage_StanleyPark.py create mode 100644 developer/Python/wg/stage_client.py diff --git a/developer/Python/wg/db_init_client_incommon.py b/developer/Python/wg/db_init_iface.py similarity index 100% rename from developer/Python/wg/db_init_client_incommon.py rename to developer/Python/wg/db_init_iface.py 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()) -- 2.20.1