#!/usr/bin/env python3 """Migrate Nginx Proxy Manager proxy hosts from SQLite into RustProxy.""" from __future__ import annotations from migrate_npm_core import * # stable migration/model API from migrate_npm_api import * # stable administration API helpers def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--database", type=Path, default=Path("/data/database.sqlite"), help="NPM SQLite database (default: /data/database.sqlite)") parser.add_argument("--rustproxy", required=True, help="RustProxy admin origin, e.g. https://rustproxy.example:9090") parser.add_argument("--username", default=os.environ.get("RUSTPROXY_USERNAME"), help="RustProxy administrator username (or RUSTPROXY_USERNAME)") parser.add_argument("--password-env", default="RUSTPROXY_PASSWORD", help="environment variable containing the RustProxy password") parser.add_argument("--ca-file", type=Path, help="CA file for RustProxy administration HTTPS") parser.add_argument("--insecure-admin-tls", action="store_true", help="disable certificate verification for the RustProxy admin HTTPS connection") parser.add_argument("--http-listener", action="append", help="RustProxy HTTP listener ID; repeat or comma-separate. Default: first enabled HTTP listener") parser.add_argument("--https-listener", action="append", help="RustProxy TLS listener ID; repeat or comma-separate. Default: first enabled TLS listener") parser.add_argument("--certificate-id", help="named RustProxy certificate for imported TLS hosts; omit to use the application default certificate") parser.add_argument("--force-https", action="store_true", help="enable RustProxy Force SSL for every imported host and attach it to HTTP+TLS listeners") parser.add_argument("--npm-compatible-backend-tls", action="store_true", help="legacy no-op: backend tls_verify already defaults to false in RustProxy 0.22.1") parser.add_argument("--plan-out", type=Path, default=Path("npm-migration-plan.json"), help="write the generated plan/config JSON here") parser.add_argument("--apply", action="store_true", help="perform the revisioned PUT /api/config; without this flag the run is read-only") args = parser.parse_args() if args.ca_file and args.insecure_admin_tls: parser.error("choose either --ca-file or --insecure-admin-tls") username = args.username or input("RustProxy username: ").strip() if not username: parser.error("RustProxy username is required") password = os.environ.get(args.password_env) if password is None: password = getpass.getpass("RustProxy password: ") try: npm_hosts = load_npm_hosts(args.database) client = RustProxyClient(args.rustproxy, ca_file=args.ca_file, insecure_tls=args.insecure_admin_tls) client.login(username, password) envelope = client.config() merged, decisions = build_migration( npm_hosts, envelope["config"], http_listener_ids=split_ids(args.http_listener), https_listener_ids=split_ids(args.https_listener), certificate_id=args.certificate_id, force_https=args.force_https, npm_compatible_backend_tls=args.npm_compatible_backend_tls, ) config_bytes = len(json.dumps(merged, separators=(",", ":"), ensure_ascii=False).encode("utf-8")) if config_bytes > MAX_CONFIG_BYTES: raise MigrationError(f"merged RustProxy config is {config_bytes} bytes; API limit is {MAX_CONFIG_BYTES} bytes") plan = { "source": {"type": "nginx-proxy-manager-sqlite", "database": str(args.database)}, "rustproxy": {"origin": args.rustproxy, "expected_revision": envelope["revision"], "config_bytes": config_bytes}, "apply_requested": bool(args.apply), "decisions": [asdict(item) for item in decisions], "config": merged, } write_private_json(args.plan_out, plan) print_report(decisions) print(f"\nPlan written: {args.plan_out}") if args.apply: imported = sum(1 for item in decisions if item.action == "import") if imported == 0: raise MigrationError("nothing to import; configuration was not written") applied = client.apply(envelope["revision"], merged) print(f"Applied RustProxy configuration revision {applied.get('revision', '?')} ({imported} NPM proxy host records imported).") else: print("Dry run only. Review the plan and rerun with --apply to write RustProxy configuration.") except (MigrationError, OSError, sqlite3.Error) as error: parser.exit(1, f"Migration failed: {error}\n") if __name__ == "__main__": main()