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