Add infra-tool: infrastructure registry with Incus container deployment
Service registry & discovery system that aggregates infrastructure metadata from Incus, K8s, APISIX, and BunnyCDN into NocoDB. Includes FastAPI HTTP API, systemd timer for 15-min auto-sync, and dual-mode collectors (REST API for container deployment, CLI/SSH fallback for local use). Deployed to jp1:infra-tool with Tailscale socket proxy for host network visibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
155
lookup.py
Normal file
155
lookup.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""Domain path tracing: CDN → Gateway → Backend."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
import click
|
||||
|
||||
import nocodb_client
|
||||
|
||||
|
||||
def _tailscale_map() -> dict[str, str]:
|
||||
"""Return {ip: hostname} from tailscale status."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["tailscale", "status", "--json"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
if out.returncode != 0:
|
||||
return {}
|
||||
data = json.loads(out.stdout)
|
||||
result = {}
|
||||
for peer in data.get("Peer", {}).values():
|
||||
name = _ts_display_name(peer)
|
||||
for addr in peer.get("TailscaleIPs", []):
|
||||
result[addr] = name
|
||||
self_node = data.get("Self", {})
|
||||
name = _ts_display_name(self_node)
|
||||
for addr in self_node.get("TailscaleIPs", []):
|
||||
result[addr] = name
|
||||
return result
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _ts_display_name(node: dict) -> str:
|
||||
"""Pick the best display name: DNSName (sans tailnet suffix) > HostName."""
|
||||
dns = node.get("DNSName", "")
|
||||
if dns:
|
||||
return dns.split(".")[0]
|
||||
return node.get("HostName", "")
|
||||
|
||||
|
||||
def _is_tailscale_ip(ip: str) -> bool:
|
||||
return ip.startswith("100.")
|
||||
|
||||
|
||||
def lookup(domain: str) -> bool:
|
||||
"""Print the full infra path for a domain. Returns True if anything found."""
|
||||
found = False
|
||||
|
||||
# 1) CDN layer — check infra_cdn_zones hostnames
|
||||
cdn_zones = nocodb_client.list_rows("infra_cdn_zones")
|
||||
for zone in cdn_zones:
|
||||
hostnames = []
|
||||
raw = zone.get("hostnames", "")
|
||||
if raw:
|
||||
try:
|
||||
hostnames = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
hostnames = [raw]
|
||||
if domain in hostnames:
|
||||
click.echo(
|
||||
f' [CDN] BunnyCDN zone "{zone.get("Title", "?")}" '
|
||||
f'→ origin {zone.get("origin_url", "?")}'
|
||||
)
|
||||
found = True
|
||||
|
||||
# 2) Gateway layer — check infra_routes host
|
||||
routes = nocodb_client.list_rows("infra_routes")
|
||||
for route in routes:
|
||||
hosts = [h.strip() for h in route.get("host", "").split(",")]
|
||||
if domain in hosts:
|
||||
nodes = route.get("upstream_nodes", "")
|
||||
plugins = route.get("plugins", "")
|
||||
click.echo(
|
||||
f' [GATEWAY] APISIX route "{route.get("Title", "?")}" → {nodes}'
|
||||
)
|
||||
if plugins:
|
||||
click.echo(f" plugins: {plugins}")
|
||||
found = True
|
||||
|
||||
# try to resolve upstream IP to a K8s/Incus backend
|
||||
_resolve_backend(nodes)
|
||||
|
||||
# 3) Direct service lookup
|
||||
services = nocodb_client.list_rows(
|
||||
"infra_services", where=f"(domain,eq,{domain})"
|
||||
)
|
||||
for svc in services:
|
||||
if svc.get("source") in ("apisix", "bunnycdn"):
|
||||
continue # already shown above
|
||||
_print_service(svc)
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
click.echo(f" Not found: {domain}")
|
||||
|
||||
return found
|
||||
|
||||
|
||||
def _resolve_backend(upstream_nodes_json: str):
|
||||
"""Try to match upstream IP to a known K8s service, Incus container, or Tailscale host."""
|
||||
if not upstream_nodes_json:
|
||||
return
|
||||
try:
|
||||
nodes = json.loads(upstream_nodes_json)
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
|
||||
for addr in nodes:
|
||||
ip = addr.split(":")[0] if ":" in addr else addr
|
||||
port = addr.split(":")[1] if ":" in addr else ""
|
||||
|
||||
# check K8s services
|
||||
k8s_services = nocodb_client.list_rows(
|
||||
"infra_services", where="(source,eq,k8s)"
|
||||
)
|
||||
for svc in k8s_services:
|
||||
if svc.get("upstream_ip", "").startswith(ip):
|
||||
_print_service(svc)
|
||||
return
|
||||
|
||||
# check Incus containers
|
||||
containers = nocodb_client.list_rows(
|
||||
"infra_containers", where=f"(ipv4,eq,{ip})"
|
||||
)
|
||||
for c in containers:
|
||||
click.echo(
|
||||
f' [BACKEND] Incus container "{c.get("Title", "?")}" '
|
||||
f'({c.get("ipv4", "?")})'
|
||||
)
|
||||
click.echo(f' status: {c.get("status", "?")}')
|
||||
return
|
||||
|
||||
# fallback: resolve Tailscale IP → hostname
|
||||
if _is_tailscale_ip(ip):
|
||||
ts_map = _tailscale_map()
|
||||
hostname = ts_map.get(ip)
|
||||
if hostname:
|
||||
dest = f"{hostname} ({ip}:{port})" if port else f"{hostname} ({ip})"
|
||||
click.echo(f" [BACKEND] Tailscale host: {dest}")
|
||||
return
|
||||
|
||||
|
||||
def _print_service(svc: dict):
|
||||
layer = svc.get("layer", "?").upper()
|
||||
source = svc.get("source", "?")
|
||||
click.echo(
|
||||
f' [{layer}] {source} "{svc.get("display_name", svc.get("Title", "?"))}" '
|
||||
f'({svc.get("namespace", "")} ns, {svc.get("cluster", "")})'
|
||||
)
|
||||
click.echo(f' upstream: {svc.get("upstream_ip", "?")}')
|
||||
click.echo(f' status: {svc.get("status", "?")}')
|
||||
Reference in New Issue
Block a user