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:
@@ -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()
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/bin/sh
|
||||
# Pull docker-compose + .env uit Proxmox LXC's via SSH (draait op NAS).
|
||||
# Vereist: Docker, Proxmox root-wachtwoord in PROXMOX_PASSWORD
|
||||
set -e
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PW="${PROXMOX_PASSWORD:-WaQTUw2t}"
|
||||
SSH_RUN() {
|
||||
docker run --rm alpine sh -c "
|
||||
apk add --no-cache openssh-client sshpass >/dev/null 2>&1
|
||||
sshpass -p '$PW' ssh -o StrictHostKeyChecking=no root@\$1 \"\$2\"
|
||||
" -- "$1" "$2"
|
||||
}
|
||||
|
||||
pull_ct() {
|
||||
host="$1"
|
||||
node="$2"
|
||||
vmid="$3"
|
||||
name="$4"
|
||||
ip="$5"
|
||||
appdir="$ROOT/apps/$name"
|
||||
mkdir -p "$appdir/config"
|
||||
|
||||
echo " → $name (CT $vmid @ $node, $ip)"
|
||||
|
||||
# docker-compose bestanden vinden en kopiëren
|
||||
SSH_RUN "$host" "pct exec $vmid -- sh -c '
|
||||
find / -maxdepth 6 \( -name docker-compose.yml -o -name docker-compose.yaml -o -name compose.yml \) 2>/dev/null | grep -v proc | head -30
|
||||
'" > /tmp/lxc-compose-list.$$ 2>/dev/null || true
|
||||
|
||||
idx=0
|
||||
while IFS= read -r fpath; do
|
||||
[ -z "$fpath" ] && continue
|
||||
idx=$((idx + 1))
|
||||
safe=$(echo "$fpath" | tr '/ ' '__')
|
||||
SSH_RUN "$host" "pct exec $vmid -- cat '$fpath'" > "$appdir/config/compose-${idx}-${safe}" 2>/dev/null || true
|
||||
dir=$(dirname "$fpath")
|
||||
SSH_RUN "$host" "pct exec $vmid -- sh -c 'for e in $dir/.env $dir/.env.local; do [ -f \"\$e\" ] && echo === \$e === && cat \"\$e\"; done'" \
|
||||
> "$appdir/config/env-${idx}-${safe}" 2>/dev/null || true
|
||||
done < /tmp/lxc-compose-list.$$
|
||||
rm -f /tmp/lxc-compose-list.$$
|
||||
|
||||
# meta
|
||||
cat > "$appdir/proxmox.meta.yaml" <<META
|
||||
# Auto-generated — Proxmox LXC
|
||||
host: $node
|
||||
proxmox_ip: $host
|
||||
vmid: $vmid
|
||||
hostname: $name
|
||||
ip: $ip
|
||||
META
|
||||
}
|
||||
|
||||
echo "Pull LXC configs → $ROOT/apps/"
|
||||
|
||||
# draaiende containers (feb 2026)
|
||||
pull_ct 192.168.1.216 pve 104 vaultwarden 192.168.1.5
|
||||
pull_ct 192.168.1.216 pve 105 linkwarden 192.168.1.142
|
||||
pull_ct 192.168.1.216 pve 107 pve-scripts-local 192.168.1.23
|
||||
pull_ct 192.168.1.216 pve 117 proxy 192.168.1.165
|
||||
pull_ct 192.168.1.216 pve 118 paymenter 192.168.1.45
|
||||
pull_ct 192.168.1.216 pve 119 nodecast-tv 192.168.1.99
|
||||
|
||||
pull_ct 192.168.1.56 proxmox 107 virtualmin 192.168.5.24
|
||||
pull_ct 192.168.1.56 proxmox 109 nginx-proxy-manager 192.168.1.173
|
||||
pull_ct 192.168.1.56 proxmox 111 pegaprox 192.168.1.249
|
||||
|
||||
echo "Klaar."
|
||||
Reference in New Issue
Block a user