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:
83
nocodb_client.py
Normal file
83
nocodb_client.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""NocoDB v2 REST helper — list / upsert / mark-unknown."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import requests
|
||||
|
||||
import config
|
||||
|
||||
|
||||
def _headers() -> dict:
|
||||
return {
|
||||
"xc-token": config.nocodb_token(),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
def _api(table: str) -> str:
|
||||
tid = config.TABLE_IDS[table]
|
||||
return f"{config.NOCODB_URL}/api/v2/tables/{tid}/records"
|
||||
|
||||
|
||||
# ── read ────────────────────────────────────────────────────────────
|
||||
|
||||
def list_rows(table: str, *, where: str = "", limit: int = 1000) -> list[dict]:
|
||||
"""Fetch all rows (paginated)."""
|
||||
rows: list[dict] = []
|
||||
offset = 0
|
||||
while True:
|
||||
params: dict = {"limit": limit, "offset": offset}
|
||||
if where:
|
||||
params["where"] = where
|
||||
resp = requests.get(_api(table), headers=_headers(), params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
batch = data.get("list", [])
|
||||
rows.extend(batch)
|
||||
pi = data.get("pageInfo", {})
|
||||
if pi.get("isLastPage", True) or not batch:
|
||||
break
|
||||
offset += limit
|
||||
return rows
|
||||
|
||||
|
||||
def find_by_title(table: str, title: str) -> dict | None:
|
||||
# URL-encode special chars in title for NocoDB where clause
|
||||
safe_title = title.replace(",", "\\,")
|
||||
rows = list_rows(table, where=f"(Title,eq,{safe_title})", limit=1)
|
||||
return rows[0] if rows else None
|
||||
|
||||
|
||||
# ── write ───────────────────────────────────────────────────────────
|
||||
|
||||
def create_row(table: str, data: dict) -> dict:
|
||||
resp = requests.post(_api(table), headers=_headers(), json=data)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def update_row(table: str, row_id: int, data: dict) -> dict:
|
||||
payload = {"Id": row_id, **data}
|
||||
resp = requests.patch(_api(table), headers=_headers(), json=payload)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def upsert(table: str, title: str, data: dict) -> str:
|
||||
"""Insert or update a row keyed by Title. Returns 'created' or 'updated'."""
|
||||
existing = find_by_title(table, title)
|
||||
if existing:
|
||||
update_row(table, existing["Id"], data)
|
||||
return "updated"
|
||||
else:
|
||||
create_row(table, {**data, "Title": title})
|
||||
return "created"
|
||||
|
||||
|
||||
def mark_unseen(table: str, seen_titles: set[str], *, source_filter: str = ""):
|
||||
"""Set status=unknown for rows not in seen_titles (scoped by source)."""
|
||||
where = f"(source,eq,{source_filter})" if source_filter else ""
|
||||
all_rows = list_rows(table, where=where)
|
||||
for row in all_rows:
|
||||
if row.get("Title") not in seen_titles and row.get("status") != "unknown":
|
||||
update_row(table, row["Id"], {"status": "unknown"})
|
||||
Reference in New Issue
Block a user