+++ /dev/null
-#!/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)
--- /dev/null
+#!/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)
# 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
# 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
--- /dev/null
+-New interface:
+
+copy `db_init_iface_x6.py` to `db_init_iface_<name>.py`, replacing <name> with the name of the interface. Then edit `db_init_iface_<name>.py`
+
+-New Client
+
+-New User
+
+
--- /dev/null
+#!/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)
--- /dev/null
+#!/usr/bin/env python3
+"""
+stage_client.py
+
+Given:
+ - A SQLite DB reachable via incommon.open_db()
+ - A client machine name (used to locate ./key/<client_machine_name> 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())