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:
120
collectors/incus.py
Normal file
120
collectors/incus.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user