from __future__ import annotations import argparse import importlib import os import sys from pathlib import Path from knoe.core.cluster_storage_browser import ( ClusterStorageRequest, GcloudRegionQuotaProvider, PingLatencyProvider, build_cluster_storage_browser_result, ) from .context import KnoeContext def _project_root() -> Path: # /knoe/ops/cli.py -> parents: ops, knoe, root return Path(__file__).resolve().parents[2] def main(argv: list[str] | None = None) -> int: argv = list(argv) if argv is not None else list(sys.argv[1:]) if argv and argv[0] == "storage-browser": storage = argparse.ArgumentParser(prog="knoe-ops storage-browser", add_help=True) storage.add_argument("--project", required=True, help="GCP project ID") storage.add_argument("--timezone", default="", help="Timezone group: americas|emea|apac") storage.add_argument("--pgdata-gb", type=float, default=100.0) storage.add_argument("--wal-gb", type=float, default=25.0) storage.add_argument( "--regions", default="", help="Comma-separated region list. If omitted, fetch via gcloud compute regions list.", ) storage.add_argument("--output", choices=["table", "json"], default="table") args = storage.parse_args(argv[1:]) regions = [r.strip() for r in (args.regions or "").split(",") if r.strip()] if not regions: import subprocess cmd = ["gcloud", "compute", "regions", "list", "--project", args.project, "--format=value(name)"] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=20) if result.returncode == 0: regions = [r.strip() for r in result.stdout.splitlines() if r.strip()] except Exception: regions = [] result = build_cluster_storage_browser_result( project_id=args.project, requested=ClusterStorageRequest(pgdata_gb=args.pgdata_gb, wal_gb=args.wal_gb), available_regions=regions, timezone_group=(args.timezone or "").strip().lower() or None, quota_provider=GcloudRegionQuotaProvider(), latency_provider=PingLatencyProvider(), ) if args.output == "json": print(result.to_json()) return 0 print(f"Project: {result.project_id} Timezone: {result.timezone_group}") print(f"Quota docs: {result.quota_link}") print(f"Cloud Hub: {result.quota_console_link}") print("region\tfeasible\tdefault(pg/wal)\tpremium_headroom\tstandard_headroom\tlatency_ms") for c in result.candidates: f = c.feasibility feasible = "yes" if (f.can_premium or f.can_standard) else "no" premium = "-" if f.premium_headroom_gb is None else f"{f.premium_headroom_gb:.1f}" standard = "-" if f.standard_headroom_gb is None else f"{f.standard_headroom_gb:.1f}" latency = "-" if c.latency_ms is None else f"{c.latency_ms:.2f}" print( f"{f.region}\t{feasible}\t{f.default_pgdata_class}/{f.default_wal_class}\t" f"{premium}\t{standard}\t{latency}" ) return 0 p = argparse.ArgumentParser(prog="knoe-ops", add_help=True) p.add_argument("component", help="component name (e.g. common_core, openbao)") p.add_argument( "action", help="action to run (initialize, start, update, restart, stop, status)", ) p.add_argument("--mode", default=os.environ.get("KNOE_MODE", "dev")) p.add_argument("--namespace", default=os.environ.get("PROLE_NAMESPACE", "default")) p.add_argument( "--service-namespace", default=os.environ.get("KNOE_SERVICE_NAMESPACE", "default"), ) p.add_argument("--config", dest="config", default=os.environ.get("PROLE_CFG")) args = p.parse_args(argv) cfg_path = Path(args.config).expanduser().resolve() if args.config else None ctx = KnoeContext( project_root=_project_root(), cfg_path=cfg_path, mode=args.mode, namespace=args.namespace, service_namespace=args.service_namespace, env=dict(os.environ), logger=lambda m: print(m), err_logger=lambda m: print(m, file=sys.stderr), ) mod = importlib.import_module(f"knoe.ops.components.{args.component}") fn = getattr(mod, args.action) rc = fn(ctx) return 0 if rc is None else int(rc) if __name__ == "__main__": # pragma: no cover raise SystemExit(main())