- Add scripts/it-ops (Proxmox collector, IPAM drift, export orchestrator) - Add sankofa-it-read-api stub with optional CORS and refresh - Add systemd examples for read API, weekly inventory export, timer - Add live-inventory-drift GitHub workflow (dispatch + weekly) - Add IT controller spec, runbooks, Keycloak ensure-it-admin-role script - Note IT_READ_API env on portal sync completion output Made-with: Cursor
110 lines
3.0 KiB
Python
Executable File
110 lines
3.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Run ON a Proxmox cluster node (as root). Stdout: JSON live guest inventory."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
def _run(cmd: list[str]) -> str:
|
|
return subprocess.check_output(cmd, text=True, stderr=subprocess.DEVNULL)
|
|
|
|
|
|
def _extract_ip_from_net_line(line: str) -> str | None:
|
|
m = re.search(r"ip=([0-9.]+)", line)
|
|
return m.group(1) if m else None
|
|
|
|
|
|
def _read_config(path: str) -> str:
|
|
try:
|
|
with open(path, encoding="utf-8", errors="replace") as f:
|
|
return f.read()
|
|
except OSError:
|
|
return ""
|
|
|
|
|
|
def main() -> None:
|
|
collected_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
try:
|
|
raw = _run(
|
|
["pvesh", "get", "/cluster/resources", "--output-format", "json"]
|
|
)
|
|
resources = json.loads(raw)
|
|
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
|
|
json.dump(
|
|
{
|
|
"collected_at": collected_at,
|
|
"error": f"pvesh_cluster_resources_failed: {e}",
|
|
"guests": [],
|
|
},
|
|
sys.stdout,
|
|
indent=2,
|
|
)
|
|
return
|
|
|
|
guests: list[dict] = []
|
|
for r in resources:
|
|
t = r.get("type")
|
|
if t not in ("lxc", "qemu"):
|
|
continue
|
|
vmid = r.get("vmid")
|
|
node = r.get("node")
|
|
if vmid is None or not node:
|
|
continue
|
|
vmid_s = str(vmid)
|
|
name = r.get("name") or ""
|
|
status = r.get("status") or ""
|
|
|
|
if t == "lxc":
|
|
cfg_path = f"/etc/pve/nodes/{node}/lxc/{vmid_s}.conf"
|
|
else:
|
|
cfg_path = f"/etc/pve/nodes/{node}/qemu-server/{vmid_s}.conf"
|
|
|
|
body = _read_config(cfg_path)
|
|
ip = ""
|
|
for line in body.splitlines():
|
|
if line.startswith("net0:"):
|
|
got = _extract_ip_from_net_line(line)
|
|
if got:
|
|
ip = got
|
|
break
|
|
if not ip and t == "qemu":
|
|
for line in body.splitlines():
|
|
if line.startswith("ipconfig0:"):
|
|
got = _extract_ip_from_net_line(line)
|
|
if got:
|
|
ip = got
|
|
break
|
|
if not ip and t == "qemu":
|
|
for line in body.splitlines():
|
|
if line.startswith("net0:"):
|
|
got = _extract_ip_from_net_line(line)
|
|
if got:
|
|
ip = got
|
|
break
|
|
|
|
guests.append(
|
|
{
|
|
"vmid": vmid_s,
|
|
"type": t,
|
|
"node": str(node),
|
|
"name": name,
|
|
"status": status,
|
|
"ip": ip,
|
|
"config_path": cfg_path,
|
|
}
|
|
)
|
|
|
|
out = {
|
|
"collected_at": collected_at,
|
|
"guests": sorted(guests, key=lambda g: int(g["vmid"])),
|
|
}
|
|
json.dump(out, sys.stdout, indent=2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|