Files
infra-tool/collectors/incus.py
kappa 5e59261f63 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>
2026-03-03 09:13:43 +09:00

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()