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>
121 lines
4.2 KiB
Python
121 lines
4.2 KiB
Python
"""Collect Incus containers via REST API or local CLI (auto-detected)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
from datetime import datetime, timezone
|
|
|
|
import requests
|
|
import urllib3
|
|
|
|
import config
|
|
|
|
# Suppress InsecureRequestWarning when using self-signed Incus certs.
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
# Used by CLI mode only; REST mode reads from config.INCUS_REMOTES.
|
|
REMOTES = {
|
|
"jp1": ["monitoring", "db", "default"],
|
|
"kr1": ["default", "inbest", "karakeep", "security"],
|
|
}
|
|
|
|
|
|
def _extract_ipv4(instance: dict) -> str:
|
|
"""Return the first global IPv4 address found in instance network state."""
|
|
net = (instance.get("state") or {}).get("network") or {}
|
|
for iface in net.values():
|
|
for addr in iface.get("addresses", []):
|
|
if addr.get("family") == "inet" and addr.get("scope") == "global":
|
|
return addr["address"]
|
|
return ""
|
|
|
|
|
|
def _collect_rest() -> list[dict]:
|
|
"""Collect containers from all Incus remotes via REST API with TLS client certs."""
|
|
results: list[dict] = []
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
cert = (config.incus_cert(), config.incus_key())
|
|
|
|
for remote, remote_cfg in config.INCUS_REMOTES.items():
|
|
base_url = remote_cfg["url"]
|
|
for project in remote_cfg["projects"]:
|
|
url = f"{base_url}/1.0/instances?project={project}&recursion=1"
|
|
try:
|
|
resp = requests.get(url, cert=cert, verify=False, timeout=30)
|
|
if not resp.ok:
|
|
print(
|
|
f"[incus] REST request failed for {remote}/{project}: "
|
|
f"{resp.status_code} {resp.status_text if hasattr(resp, 'status_text') else resp.reason}"
|
|
)
|
|
continue
|
|
data = resp.json()
|
|
containers = data.get("metadata") or []
|
|
except requests.RequestException as exc:
|
|
print(f"[incus] REST request error for {remote}/{project}: {exc}")
|
|
continue
|
|
except (ValueError, KeyError) as exc:
|
|
print(f"[incus] REST response parse error for {remote}/{project}: {exc}")
|
|
continue
|
|
|
|
for c in containers:
|
|
name = c.get("name", "")
|
|
results.append({
|
|
"Title": f"{remote}/{project}/{name}",
|
|
"name": name,
|
|
"remote": remote,
|
|
"project": project,
|
|
"status": c.get("status", "Unknown"),
|
|
"ipv4": _extract_ipv4(c),
|
|
"last_synced": now,
|
|
})
|
|
|
|
return results
|
|
|
|
|
|
def _collect_cli() -> list[dict]:
|
|
"""Collect containers from all Incus remotes via local CLI subprocess."""
|
|
results: list[dict] = []
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
for remote, projects in REMOTES.items():
|
|
for project in projects:
|
|
try:
|
|
out = subprocess.run(
|
|
["incus", "list", f"{remote}:", f"--project={project}", "--format=json"],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
if out.returncode != 0:
|
|
continue
|
|
containers = json.loads(out.stdout)
|
|
except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):
|
|
continue
|
|
|
|
for c in containers:
|
|
name = c.get("name", "")
|
|
results.append({
|
|
"Title": f"{remote}/{project}/{name}",
|
|
"name": name,
|
|
"remote": remote,
|
|
"project": project,
|
|
"status": c.get("status", "Unknown"),
|
|
"ipv4": _extract_ipv4(c),
|
|
"last_synced": now,
|
|
})
|
|
|
|
return results
|
|
|
|
|
|
def collect() -> list[dict]:
|
|
"""Return list of container records for NocoDB infra_containers.
|
|
|
|
Uses REST API when TLS client certificates are available,
|
|
falls back to local CLI otherwise.
|
|
"""
|
|
if config.incus_certs_available():
|
|
print("[incus] Using REST API mode")
|
|
return _collect_rest()
|
|
|
|
print("[incus] Using CLI mode (certs not available)")
|
|
return _collect_cli()
|