prole/knoe/ops/cli.py

116 lines
4.5 KiB
Python

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