Files
RustProxy/scripts/migration/migrate_npm.py
T
2026-10-11 22:12:27 +02:00

76 lines
4.7 KiB
Python
Executable File

#!/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()