Pull Proxmox LXC app configs via SSH and document all CTs.

Add pull-lxc-from-proxmox.py using Proxmox API + pct exec for running
containers (vaultwarden, linkwarden, paymenter, NPM, etc). Stub apps for
stopped LXCs with proxmox.meta.yaml and updated lxc-inventory with live IPs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mo
2026-05-17 14:52:28 +02:00
parent c7f1b094cb
commit 9f431ff97b
85 changed files with 1392 additions and 37 deletions
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""Pull LXC configs from Proxmox via SSH+pct (runs on NAS)."""
import json, os, re, ssl, subprocess, urllib.parse, urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
APPS = ROOT / "apps"
PASSWORD = os.environ.get("PROXMOX_PASSWORD", "WaQTUw2t")
HOSTS = [("192.168.1.216", "pve"), ("192.168.1.56", "proxmox")]
ssl._create_default_https_context = ssl._create_unverified_context
def ssh(host: str, cmd: str) -> str:
inner = f"apk add --no-cache openssh-client sshpass >/dev/null 2>&1 && sshpass -p '{PASSWORD}' ssh -o StrictHostKeyChecking=no root@{host} {json.dumps(cmd)}"
r = subprocess.run(["docker", "run", "--rm", "alpine", "sh", "-c", inner], capture_output=True, text=True, timeout=120)
return r.stdout if r.returncode == 0 else ""
def pve_login(host: str):
data = urllib.parse.urlencode({"username": "root@pam", "password": PASSWORD}).encode()
req = urllib.request.Request(f"https://{host}:8006/api2/json/access/ticket", data=data, method="POST")
with urllib.request.urlopen(req, timeout=15) as r:
a = json.loads(r.read())["data"]
return a["ticket"], a["CSRFPreventionToken"]
def pve_get(host, path, ticket, csrf):
headers = {"Cookie": f"PVEAuthCookie={ticket}", "CSRFPreventionToken": csrf}
req = urllib.request.Request(f"https://{host}:8006/api2/json{path}", headers=headers)
with urllib.request.urlopen(req, timeout=15) as r:
return json.loads(r.read())["data"]
def slug(name: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
def lxc_ip(host, node, vmid, ticket, csrf):
try:
ifaces = pve_get(host, f"/nodes/{node}/lxc/{vmid}/interfaces", ticket, csrf)
for iface in ifaces:
for addr in iface.get("ip-addresses", []):
if addr.get("ip-address-type") == "inet" and not addr["ip-address"].startswith("127."):
return addr["ip-address"]
except Exception:
pass
return ""
def pull_running(host, node, vmid, name, ip):
sname = slug(name) or f"ct-{vmid}"
appdir = APPS / sname
cfg = appdir / "config"
cfg.mkdir(parents=True, exist_ok=True)
find_cmd = f"pct exec {vmid} -- sh -c 'find /opt /root /data /vaultwarden /home -maxdepth 5 \\( -name docker-compose.yml -o -name docker-compose.yaml -o -name compose.yml -o -name .env \\) 2>/dev/null | head -40'"
files = [f.strip() for f in ssh(host, find_cmd).splitlines() if f.strip()]
for i, fpath in enumerate(files, 1):
safe = re.sub(r"[^a-zA-Z0-9._-]", "_", fpath)[:80]
content = ssh(host, f"pct exec {vmid} -- cat {json.dumps(fpath)}")
if content.strip():
(cfg / f"{i:02d}-{safe}").write_text(content)
# NPM data snapshot
if "nginx" in sname or vmid == 109:
snap = ssh(host, f"pct exec {vmid} -- sh -c 'ls -la /data 2>/dev/null; ls /data/nginx 2>/dev/null'")
if snap.strip():
(cfg / "npm-data-listing.txt").write_text(snap)
# pve-scripts
if "script" in sname:
listing = ssh(host, f"pct exec {vmid} -- sh -c 'find /opt/ProxmoxVE-Local -maxdepth 2 -type f 2>/dev/null | head -30'")
if listing.strip():
(cfg / "proxmoxve-local-files.txt").write_text(listing)
meta = f"""# Auto-generated
host: {node}
proxmox_ip: {host}
vmid: {vmid}
hostname: {name}
ip: {ip}
status: running
"""
(appdir / "proxmox.meta.yaml").write_text(meta)
readme = f"""# {name}
| | |
|---|---|
| **Proxmox** | {node} (CT {vmid}) |
| **IP** | {ip or 'dhcp'} |
| **Host** | {host} |
Config in `config/` (gepull'd van LXC).
```bash
# Op Proxmox host:
pct enter {vmid}
```
"""
(appdir / "README.md").write_text(readme)
print(f" pulled {sname} ({ip})")
def stub_stopped(host, node, vmid, name):
sname = slug(name) or f"ct-{vmid}"
appdir = APPS / sname
if (appdir / "config").exists() and any((appdir / "config").iterdir()):
return # already pulled when was running
appdir.mkdir(parents=True, exist_ok=True)
(appdir / "config").mkdir(exist_ok=True)
meta = f"""# Auto-generated
host: {node}
proxmox_ip: {host}
vmid: {vmid}
hostname: {name}
status: stopped
"""
(appdir / "proxmox.meta.yaml").write_text(meta)
readme = f"""# {name}
| | |
|---|---|
| **Proxmox** | {node} (CT {vmid}) |
| **Status** | gestopt |
Container-definitie: `apps/proxmox/hosts/{node}/lxc/{vmid}.conf`
Start CT en draai `scripts/pull-lxc-from-proxmox.py` opnieuw om app-config te pullen.
"""
(appdir / "README.md").write_text(readme)
def main():
print(f"Pull → {APPS}")
seen = set()
for host, node in HOSTS:
ticket, csrf = pve_login(host)
lxcs = pve_get(host, f"/nodes/{node}/lxc", ticket, csrf)
for ct in lxcs:
vmid = ct["vmid"]
name = ct.get("name") or f"ct-{vmid}"
sname = slug(name)
if sname in seen:
sname = f"{sname}-{node}"
seen.add(sname)
ip = lxc_ip(host, node, vmid, ticket, csrf) if ct["status"] == "running" else ""
if ct["status"] == "running":
pull_running(host, node, vmid, name, ip)
else:
stub_stopped(host, node, vmid, name)
print("Klaar.")
if __name__ == "__main__":
main()