Add Proxmox inventory scripts and n8n sync workflow
- proxmox-inventory.sh: text inventory (RAM/CPU/Swap/Disk/IP/Gateway/Subnetz) - proxmox-inventory-json.sh: JSON variant with n8n webhook push, --vmids filter, --dry-run - n8n workflow: automated fact-sync between Proxmox and Outline docs, auto-creates pages for new VMIDs, ntfy summary notification - credentials (Outline/NTFY bearer tokens) intentionally excluded, see README
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Auto Outline Doku Proxmox Inventory
|
||||||
|
|
||||||
|
Automatischer Abgleich zwischen Proxmox-Inventar und der Homelab-Dokumentation in Outline.
|
||||||
|
|
||||||
|
## Inhalt
|
||||||
|
|
||||||
|
- `scripts/proxmox-inventory.sh` – Text-Ausgabe des Proxmox-Inventars (VMID, Status, Ressourcen, Netzwerk inkl. Subnetzmaske/Gateway), zum manuellen Ausführen und Copy-Paste.
|
||||||
|
- `scripts/proxmox-inventory-json.sh` – Baut dieselben Daten als JSON und sendet sie per `curl` an einen n8n-Webhook. Unterstützt `--dry-run`, `--print-only` und `--vmids=101,102,...` zum gezielten Testen einzelner Container/VMs.
|
||||||
|
- `n8n-workflows/auto-outline-doku-proxmox-inventory.json` – Exportierter n8n-Workflow, der die vom Script gesendeten Daten mit Outline abgleicht: Fakten (RAM, CPU, Swap, Disk, IP, Gateway, Subnetz, MAC) patchen, neue VMIDs automatisch als Outline-Seite anlegen, und eine ntfy-Benachrichtigung mit Zusammenfassung senden.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. **Script konfigurieren:** In `scripts/proxmox-inventory-json.sh` die Variable `N8N_WEBHOOK_URL` auf die echte n8n-Webhook-URL setzen (aktuell ein Platzhalter, da diese pro Installation individuell ist).
|
||||||
|
2. **n8n-Workflow importieren:** JSON-Datei in n8n importieren. Folgende Credentials werden benötigt (nicht im Export enthalten, aus Sicherheitsgründen):
|
||||||
|
- **Outline Bearer Auth** (Header Auth, `Authorization: Bearer <Outline-API-Token>`) – für alle `outline.vogt.de.com/api/...`-Requests
|
||||||
|
- **NTFY Bearer Auth** (Header Auth) – für die ntfy-Benachrichtigung am Ende
|
||||||
|
3. **Webhook-Pfad in n8n** mit der URL im Script abgleichen.
|
||||||
|
4. Erst mit `--vmids=<einzelne-VMID> --dry-run` testen, bevor der volle Sync scharf geschaltet wird.
|
||||||
|
|
||||||
|
## Funktionsweise (Kurzfassung)
|
||||||
|
|
||||||
|
- Script sammelt Proxmox-Daten (LXC + VM) → sendet JSON an n8n
|
||||||
|
- n8n sucht pro VMID die passende Outline-Seite (`documents.search`), lädt den aktuellen Inhalt separat nach (`documents.info`, um Suchindex-Verzögerungen zu vermeiden)
|
||||||
|
- Bei Abweichungen: gezielter Patch nur der betroffenen Felder (`documents.update`, editMode `patch`)
|
||||||
|
- Bei keinem Treffer: neue Seite wird automatisch aus den bekannten Fakten angelegt (`documents.create`), unbekannte Felder wie Zweck/Ports bleiben als Platzhalter
|
||||||
|
- Am Ende: Zusammenfassung aller Änderungen/Auffälligkeiten per ntfy
|
||||||
|
|
||||||
|
## Bewusst nicht automatisiert
|
||||||
|
|
||||||
|
- Kategorisierung, Zweck-Beschreibung, Ports/URLs in der zentralen Übersichtstabelle (nicht aus Proxmox ableitbar)
|
||||||
|
- Umbenennen/Archivieren bei VMID-Wiederverwendung (z. B. wenn eine VMID gelöscht und für einen neuen Container wiederverwendet wird) – das bleibt manuelle Review-Arbeit
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
{
|
||||||
|
"name": "Auto Outline Doku Proxmox Inventory",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": { "httpMethod": "POST", "path": "22ac0717-01fa-4756-a45b-2dc7411a6442", "options": {} },
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [-688, -160],
|
||||||
|
"id": "9cda8c79-dc79-4b34-b972-ac7247d8f451",
|
||||||
|
"name": "Webhook",
|
||||||
|
"webhookId": "22ac0717-01fa-4756-a45b-2dc7411a6442"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const body = $input.first().json.body;\nconst containers = (body.containers || []).map(c => ({ ...c, category: 'lxc' }));\nconst vms = (body.vms || []).map(v => ({ ...v, category: 'vm' }));\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.summary = (body.missing_vmids || []).map(m => ({\n vmid: m.vmid,\n name: '-',\n changes: [`⚠️ ${m.reason}`]\n}));\n\nreturn [...containers, ...vms].map(item => ({ json: item }));"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [-480, -160],
|
||||||
|
"id": "1468b5ca-5379-451c-a648-9673049e43b7",
|
||||||
|
"name": "Code in JavaScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": { "options": {} },
|
||||||
|
"type": "n8n-nodes-base.splitInBatches",
|
||||||
|
"typeVersion": 3,
|
||||||
|
"position": [-272, -160],
|
||||||
|
"id": "db3b4738-4183-4d27-8d8b-6c03cc880cc8",
|
||||||
|
"name": "Loop Over Items"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const item = $('Loop Over Items').item.json;\nconst doc = $json.data;\nconst text = doc.text || '';\n\nconst patches = [];\nconst changedFields = [];\n\nfunction formatDisk(raw) {\n if (!raw) return raw;\n const m = String(raw).match(/^(\\d+(?:\\.\\d+)?)\\s*([KMGT])$/i);\n if (!m) return raw;\n return `${m[1]} ${m[2].toUpperCase()}B`;\n}\n\nfunction replaceField(fieldLabel, newValue, unit) {\n if (newValue === null || newValue === undefined || newValue === '') return;\n const re = new RegExp(`(\\\\|\\\\s*${fieldLabel}\\\\s*\\\\|\\\\s*)([^|]+?)(\\\\s*\\\\|)`, 'i');\n const match = text.match(re);\n if (!match) return;\n const currentValue = match[2].trim();\n const desiredValue = `${newValue}${unit || ''}`;\n if (currentValue !== desiredValue) {\n patches.push({ findText: match[0], newText: `${match[1]}${desiredValue}${match[3]}` });\n changedFields.push(`${fieldLabel.replace(/\\\\\\\\/g, '')}: ${currentValue} → ${desiredValue}`);\n }\n}\n\nreplaceField('RAM', item.memory_mb, ' MB');\nreplaceField('CPU \\\\(Cores\\\\)', item.cores);\nreplaceField('Swap', item.swap_mb, ' MB');\nreplaceField('Disk', formatDisk(item.disk));\nreplaceField('Storage-Pool', item.storage_pool);\nreplaceField('IP-Adresse', item.ip);\nreplaceField('Gateway', item.gateway);\nreplaceField('Subnetz', item.netmask);\nreplaceField('MAC-Adresse', item.mac);\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.summary.push({\n vmid: item.vmid,\n name: item.hostname,\n changes: changedFields.length > 0 ? changedFields : ['keine Änderung']\n});\n\nif (patches.length === 0) {\n return [{ json: { skip: true } }];\n}\n\nreturn patches.map(p => ({ json: { skip: false, documentId: doc.id, findText: p.findText, newText: p.newText } }));"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [816, -160],
|
||||||
|
"id": "4e81b357-2dc3-41ba-9615-98bcabb08fe9",
|
||||||
|
"name": "Code in JavaScript1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "https://outline.vogt.de.com/api/documents.search",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpBearerAuth",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "= {\n \"query\": \"{{$json.vmid}} - {{$json.type}}\",\n \"limit\": 5\n }",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.4,
|
||||||
|
"position": [-32, -144],
|
||||||
|
"id": "b7a179bd-562d-41ab-beda-0f10fbce5cac",
|
||||||
|
"name": "HTTP Request Outline Search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "https://outline.vogt.de.com/api/documents.update",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpBearerAuth",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={\n \"id\": \"{{$json.documentId}}\",\n \"editMode\": \"patch\",\n \"findText\": \"{{$json.findText}}\",\n \"text\": \"{{$json.newText}}\"\n }",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.4,
|
||||||
|
"position": [1280, -176],
|
||||||
|
"id": "01ef3d27-495c-4b0a-b93b-4144381e6a29",
|
||||||
|
"name": "HTTP Request Outline Update"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "https://outline.vogt.de.com/api/documents.create",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpBearerAuth",
|
||||||
|
"sendBody": true,
|
||||||
|
"bodyParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{ "name": "title", "value": "={{ $json.title }}" },
|
||||||
|
{ "name": "text", "value": "={{ $json.text }}" },
|
||||||
|
{ "name": "collectionId", "value": "a8414e47-ec1a-4852-b8cc-5e488288facc" },
|
||||||
|
{ "name": "parentDocumentId", "value": "fd591d15-8f31-40ff-8db6-6d442b0a6c14" },
|
||||||
|
{ "name": "publish", "value": "={{true}}" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.4,
|
||||||
|
"position": [1280, 64],
|
||||||
|
"id": "7732e2d0-83bf-4150-bdb6-562f89d4b231",
|
||||||
|
"name": "HTTP Request Outline Create"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const item = $('Loop Over Items').item.json;\nconst data = $json.data || [];\nconst hasMatch = data.length > 0 &&\n data[0].document.title.toLowerCase().includes((item.hostname || '').toLowerCase());\n\nreturn [{ json: { ...$json, hasMatch } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [176, -144],
|
||||||
|
"id": "6538f5ac-cdeb-47b4-ae99-bcf8c45fb959",
|
||||||
|
"name": "Code in JavaScript2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const item = $('Loop Over Items').item.json;\n\nconst title = `${item.vmid} - ${item.type} - ${item.hostname}`;\n\nconst text = `## Übersicht\n\n| Feld | Wert |\n|------|------|\n| VMID | ${item.vmid} |\n| Typ | ${item.type} |\n| Hostname | ${item.hostname} |\n| Proxmox-Node | ${item.proxmox_node || '-'} |\n| Template/Image | - |\n\n## Zweck\n\n*(bitte ergänzen)*\n\n## Ressourcen\n\n| Ressource | Wert |\n|-----------|------|\n| RAM | ${item.memory_mb || '-'} MB |\n| CPU (Cores) | ${item.cores || '-'} |\n| Swap | ${item.swap_mb || '-'} MB |\n| Disk | ${item.disk || '-'} |\n| Storage-Pool | ${item.storage_pool || '-'} |\n\n## Netzwerk\n\n| Feld | Wert |\n|------|------|\n| IP-Adresse | ${item.ip || '-'} |\n| Subnetz | ${item.netmask || '-'} |\n| Gateway | ${item.gateway || '-'} |\n| MAC-Adresse | ${item.mac || '-'} |\n| DNS-Name | - |\n| URL / Domain | - |\n| Reverse Proxy | - |\n\n### Ports & Dienste\n\n| Port | Protokoll | Dienst | Zugriff von |\n|------|-----------|--------|-------------|\n| | | | |\n\n## Installierte Software / Dienste\n\n* \\\\\\\\\n\n## Installation / Setup\n\n\\`\\`\\`bash\n# Befehle zur Einrichtung\n\\`\\`\\`\n\n## Konfigurationsdateien\n\n| Pfad | Zweck |\n|------|-------|\n| | |\n\n## Zugangsdaten\n\n> Zugangsdaten liegen in Vaultwarden unter: \\`<Ordner/Eintrag>\\`\n\n## Abhängigkeiten\n\n* Benötigt:\n* Wird benötigt von:\n\n## Backup\n\n| Feld | Wert |\n|------|------|\n| Methode | |\n| Zeitplan | |\n| Ziel-Storage | |\n| Retention | |\n\n## Wartung\n\n| Feld | Wert |\n|------|------|\n| Update-Befehl | |\n| Letztes Update | |\n\n## Troubleshooting / Bekannte Probleme\n\n* \\\\\\\\\n\n## Changelog\n\n| Datum | Änderung |\n|-------|----------|\n| ${new Date().toISOString().slice(0,10)} | Automatisch angelegt per Proxmox-Inventar-Sync (VMID war nicht in Outline dokumentiert) |\n`;\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.summary.push({\n vmid: item.vmid,\n name: item.hostname,\n changes: ['Neue Seite automatisch angelegt (bitte Zweck/Ports ergänzen)']\n});\n\nreturn [{ json: { title, text } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [816, 64],
|
||||||
|
"id": "4a06bba8-c41b-4234-8290-747dcd1df0b9",
|
||||||
|
"name": "Code in JavaScript3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "https://ntfy.vogt.de.com/outline",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpBearerAuth",
|
||||||
|
"sendHeaders": true,
|
||||||
|
"headerParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{ "name": "Title", "value": "Outline Benachrichtigung" },
|
||||||
|
{ "name": "Priority", "value": "default" },
|
||||||
|
{ "name": "Tags", "value": "cd" },
|
||||||
|
{ "name": "Markdown", "value": "yes" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sendBody": true,
|
||||||
|
"contentType": "raw",
|
||||||
|
"rawContentType": "html",
|
||||||
|
"body": "={{ $json.message }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.4,
|
||||||
|
"position": [176, -320],
|
||||||
|
"id": "878babb3-ad66-4df6-a9b3-fbed56087f56",
|
||||||
|
"name": "HTTP Request Send NTFY Update"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const staticData = $getWorkflowStaticData('global');\nconst summary = staticData.summary || [];\n\nconst changedOrMissing = summary.filter(s => !s.changes.includes('keine Änderung'));\n\nlet message = `Proxmox → Outline Sync\\n${summary.length} geprüft, ${changedOrMissing.length} Änderung(en)`;\n\nif (changedOrMissing.length > 0) {\n message += '\\n\\n' + changedOrMissing.map(s => {\n const isMissing = s.changes[0].startsWith('⚠️');\n const label = isMissing ? `VMID ${s.vmid}` : `${s.vmid} (${s.name})`;\n return `${label}: ${s.changes.join(', ')}`;\n }).join('\\n');\n} else {\n message += '\\nAlles synchron, keine Änderungen nötig.';\n}\n\nreturn [{\n json: {\n total: summary.length,\n changed: changedOrMissing.length,\n details: summary,\n message\n }\n}];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [-32, -320],
|
||||||
|
"id": "a6b8da76-fff4-4db0-96a1-10dbb4ace06d",
|
||||||
|
"name": "Code in JavaScript4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "https://outline.vogt.de.com/api/documents.info",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpBearerAuth",
|
||||||
|
"sendBody": true,
|
||||||
|
"bodyParameters": { "parameters": [ { "name": "id", "value": "={{$json.data[0].document.id}}" } ] },
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.4,
|
||||||
|
"position": [592, -160],
|
||||||
|
"id": "623dde11-4468-4f93-a7d5-a7428f602e45",
|
||||||
|
"name": "HTTP Request Doc Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 3 },
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "53ad57ed-cc43-43f8-ad99-c6137828d4e0",
|
||||||
|
"leftValue": "={{ $json.hasMatch }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": { "type": "boolean", "operation": "true", "singleValue": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"looseTypeValidation": true,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.3,
|
||||||
|
"position": [368, -144],
|
||||||
|
"id": "825d6f0b-a29b-4148-8109-33e61b150a94",
|
||||||
|
"name": "If match"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 },
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "821e3706-a250-42ca-8d38-7f430417346b",
|
||||||
|
"leftValue": "={{ $json.skip }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": { "type": "boolean", "operation": "false", "singleValue": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.3,
|
||||||
|
"position": [1024, -160],
|
||||||
|
"id": "328ee7b8-62e4-4776-8eb2-f6c4650d8acb",
|
||||||
|
"name": "If1 skip"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Webhook": { "main": [[{ "node": "Code in JavaScript", "type": "main", "index": 0 }]] },
|
||||||
|
"Code in JavaScript": { "main": [[{ "node": "Loop Over Items", "type": "main", "index": 0 }]] },
|
||||||
|
"Loop Over Items": { "main": [[{ "node": "Code in JavaScript4", "type": "main", "index": 0 }], [{ "node": "HTTP Request Outline Search", "type": "main", "index": 0 }]] },
|
||||||
|
"Code in JavaScript1": { "main": [[{ "node": "If1 skip", "type": "main", "index": 0 }]] },
|
||||||
|
"HTTP Request Outline Search": { "main": [[{ "node": "Code in JavaScript2", "type": "main", "index": 0 }]] },
|
||||||
|
"HTTP Request Outline Update": { "main": [[{ "node": "Loop Over Items", "type": "main", "index": 0 }]] },
|
||||||
|
"HTTP Request Outline Create": { "main": [[{ "node": "Loop Over Items", "type": "main", "index": 0 }]] },
|
||||||
|
"Code in JavaScript2": { "main": [[{ "node": "If match", "type": "main", "index": 0 }]] },
|
||||||
|
"Code in JavaScript3": { "main": [[{ "node": "HTTP Request Outline Create", "type": "main", "index": 0 }]] },
|
||||||
|
"HTTP Request Doc Info": { "main": [[{ "node": "Code in JavaScript1", "type": "main", "index": 0 }]] },
|
||||||
|
"If match": { "main": [[{ "node": "HTTP Request Doc Info", "type": "main", "index": 0 }], [{ "node": "Code in JavaScript3", "type": "main", "index": 0 }]] },
|
||||||
|
"If1 skip": { "main": [[{ "node": "HTTP Request Outline Update", "type": "main", "index": 0 }], [{ "node": "Loop Over Items", "type": "main", "index": 0 }]] },
|
||||||
|
"Code in JavaScript4": { "main": [[{ "node": "HTTP Request Send NTFY Update", "type": "main", "index": 0 }]] }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# proxmox-inventory-json.sh
|
||||||
|
# ---------------------------
|
||||||
|
# Baut dasselbe Inventar wie proxmox-inventory.sh / inventory.sh, aber als
|
||||||
|
# JSON, und sendet es per curl an einen n8n-Webhook. Das bestehende
|
||||||
|
# Text-Script bleibt unverändert für manuelle Ausführung nutzbar.
|
||||||
|
#
|
||||||
|
# Voraussetzung: "jq" muss installiert sein.
|
||||||
|
# apt install -y jq
|
||||||
|
#
|
||||||
|
# Nutzung:
|
||||||
|
# chmod +x proxmox-inventory-json.sh
|
||||||
|
# ./proxmox-inventory-json.sh # baut JSON + sendet an n8n (alle VMIDs)
|
||||||
|
# ./proxmox-inventory-json.sh --dry-run # baut JSON, zeigt es nur an, sendet NICHT
|
||||||
|
# ./proxmox-inventory-json.sh --print-only > inventory.json # nur JSON in Datei
|
||||||
|
# ./proxmox-inventory-json.sh --vmids=101,102,138 # nur diese VMIDs verarbeiten (schnell zum Testen)
|
||||||
|
#
|
||||||
|
# Alternativ dauerhaft in der VMID_FILTER-Variable unten eintragen, z.B. VMID_FILTER="101,102,138".
|
||||||
|
# Leer lassen ("") um wie gewohnt ALLE Container/VMs zu verarbeiten.
|
||||||
|
# --vmids= überschreibt die Variable, falls beides gesetzt ist.
|
||||||
|
#
|
||||||
|
# Geplant ausführen (Cron, z. B. täglich 06:00 Uhr):
|
||||||
|
# crontab -e
|
||||||
|
# 0 6 * * * /root/proxmox-inventory-json.sh >> /var/log/proxmox-inventory.log 2>&1
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# KONFIGURATION - hier eure n8n-Webhook-URL eintragen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
N8N_WEBHOOK_URL="https://n8n.example.com/webhook/REPLACE_ME"
|
||||||
|
|
||||||
|
# Komma-getrennte Liste von VMIDs, die verarbeitet werden sollen, z.B. "101,102,138"
|
||||||
|
# Leer lassen ("") um wie gewohnt ALLE Container/VMs zu verarbeiten.
|
||||||
|
VMID_FILTER=""
|
||||||
|
|
||||||
|
DRY_RUN=false
|
||||||
|
PRINT_ONLY=false
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dry-run) DRY_RUN=true ;;
|
||||||
|
--print-only) PRINT_ONLY=true ;;
|
||||||
|
--vmids=*) VMID_FILTER="${arg#--vmids=}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Prüft, ob eine VMID laut Filter verarbeitet werden soll.
|
||||||
|
# Ohne Filter (VMID_FILTER leer) wird alles verarbeitet.
|
||||||
|
should_process() {
|
||||||
|
local vmid="$1"
|
||||||
|
[ -z "$VMID_FILTER" ] && return 0
|
||||||
|
case ",${VMID_FILTER}," in
|
||||||
|
*",${vmid},"*) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
echo "'jq' ist nicht installiert - versuche automatische Installation..." >&2
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
apt-get update -qq && apt-get install -y -qq jq
|
||||||
|
fi
|
||||||
|
command -v jq >/dev/null 2>&1 || { echo "FEHLER: 'jq' konnte nicht automatisch installiert werden. Bitte manuell ausführen: apt install -y jq" >&2; exit 1; }
|
||||||
|
echo "'jq' erfolgreich installiert." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
cidr_to_netmask() {
|
||||||
|
local cidr="$1"
|
||||||
|
if ! [[ "$cidr" =~ ^[0-9]+$ ]] || [ "$cidr" -lt 0 ] || [ "$cidr" -gt 32 ]; then
|
||||||
|
echo ""
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local mask=0
|
||||||
|
if [ "$cidr" -gt 0 ]; then
|
||||||
|
mask=$(( 0xffffffff ^ ((1 << (32 - cidr)) - 1) ))
|
||||||
|
fi
|
||||||
|
printf "%d.%d.%d.%d" \
|
||||||
|
"$(( (mask >> 24) & 255 ))" \
|
||||||
|
"$(( (mask >> 16) & 255 ))" \
|
||||||
|
"$(( (mask >> 8) & 255 ))" \
|
||||||
|
"$(( mask & 255 ))"
|
||||||
|
}
|
||||||
|
|
||||||
|
TMP_LXC=$(mktemp)
|
||||||
|
TMP_VM=$(mktemp)
|
||||||
|
trap 'rm -f "$TMP_LXC" "$TMP_VM"' EXIT
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# LXC Container
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
for VMID in $(pct list | awk 'NR>1 {print $1}'); do
|
||||||
|
should_process "$VMID" || continue
|
||||||
|
NAME=$(pct list | awk -v id="$VMID" '$1==id{print $NF}')
|
||||||
|
STATUS=$(pct status "$VMID" 2>/dev/null | awk '{print $2}')
|
||||||
|
CONFIG=$(pct config "$VMID" 2>/dev/null || true)
|
||||||
|
HOSTNAME_CFG=$(echo "$CONFIG" | grep -E '^hostname:' | cut -d' ' -f2- || echo "")
|
||||||
|
CORES=$(echo "$CONFIG" | grep -E '^cores:' | cut -d' ' -f2- || echo "")
|
||||||
|
MEMORY=$(echo "$CONFIG" | grep -E '^memory:' | cut -d' ' -f2- || echo "")
|
||||||
|
SWAP=$(echo "$CONFIG" | grep -E '^swap:' | cut -d' ' -f2- || echo "")
|
||||||
|
DISK=$(echo "$CONFIG" | grep -E '^rootfs:' | grep -oP 'size=\K[^,]+' || echo "")
|
||||||
|
STORAGE_POOL=$(echo "$CONFIG" | grep -E '^rootfs:' | cut -d' ' -f2- | cut -d: -f1 || echo "")
|
||||||
|
|
||||||
|
NET0_LINE=$(echo "$CONFIG" | grep -E '^net0:' || true)
|
||||||
|
CFG_GW=$(echo "$NET0_LINE" | grep -oP 'gw=\K[0-9.]+' || echo "")
|
||||||
|
CFG_CIDR=$(echo "$NET0_LINE" | grep -oP 'ip=[0-9.]+/\K[0-9]+' || echo "")
|
||||||
|
MAC=$(echo "$NET0_LINE" | grep -oP 'hwaddr=\K[0-9A-Fa-f:]+' || echo "")
|
||||||
|
|
||||||
|
IP=""
|
||||||
|
LIVE_GW=""
|
||||||
|
LIVE_CIDR=""
|
||||||
|
if [ "$STATUS" = "running" ]; then
|
||||||
|
IP=$(pct exec "$VMID" -- ip -4 -o addr show scope global 2>/dev/null | head -1 | awk '{print $4}' | cut -d/ -f1 || echo "")
|
||||||
|
LIVE_CIDR=$(pct exec "$VMID" -- ip -4 -o addr show scope global 2>/dev/null | head -1 | awk '{print $4}' | cut -d/ -f2 || echo "")
|
||||||
|
LIVE_GW=$(pct exec "$VMID" -- ip -4 route show default 2>/dev/null | awk '{print $3}' | head -1 || echo "")
|
||||||
|
fi
|
||||||
|
|
||||||
|
GW="${LIVE_GW:-$CFG_GW}"
|
||||||
|
CIDR="${LIVE_CIDR:-$CFG_CIDR}"
|
||||||
|
NETMASK=""
|
||||||
|
[ -n "$CIDR" ] && NETMASK=$(cidr_to_netmask "$CIDR")
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--argjson vmid "$VMID" \
|
||||||
|
--arg type "lxc" \
|
||||||
|
--arg hostname "${HOSTNAME_CFG:-$NAME}" \
|
||||||
|
--arg status "$STATUS" \
|
||||||
|
--arg node "$(hostname)" \
|
||||||
|
--arg cores "${CORES:-null}" \
|
||||||
|
--arg memory_mb "${MEMORY:-null}" \
|
||||||
|
--arg swap_mb "${SWAP:-null}" \
|
||||||
|
--arg disk "$DISK" \
|
||||||
|
--arg storage_pool "$STORAGE_POOL" \
|
||||||
|
--arg ip "$IP" \
|
||||||
|
--arg gateway "$GW" \
|
||||||
|
--arg netmask "$NETMASK" \
|
||||||
|
--arg mac "$MAC" \
|
||||||
|
'{
|
||||||
|
vmid: $vmid,
|
||||||
|
type: $type,
|
||||||
|
hostname: $hostname,
|
||||||
|
status: $status,
|
||||||
|
proxmox_node: $node,
|
||||||
|
cores: (if $cores == "null" then null else ($cores|tonumber) end),
|
||||||
|
memory_mb: (if $memory_mb == "null" then null else ($memory_mb|tonumber) end),
|
||||||
|
swap_mb: (if $swap_mb == "null" then null else ($swap_mb|tonumber) end),
|
||||||
|
disk: $disk,
|
||||||
|
storage_pool: $storage_pool,
|
||||||
|
ip: $ip,
|
||||||
|
gateway: $gateway,
|
||||||
|
netmask: $netmask,
|
||||||
|
mac: $mac
|
||||||
|
}' >> "$TMP_LXC"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# VMs
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
for VMID in $(qm list | awk 'NR>1 {print $1}'); do
|
||||||
|
should_process "$VMID" || continue
|
||||||
|
NAME=$(qm list | awk -v id="$VMID" '$1==id{print $2}')
|
||||||
|
STATUS=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
|
||||||
|
CONFIG=$(qm config "$VMID" 2>/dev/null || true)
|
||||||
|
CORES=$(echo "$CONFIG" | grep -E '^cores:' | cut -d' ' -f2- || echo "")
|
||||||
|
MEMORY=$(echo "$CONFIG" | grep -E '^memory:' | cut -d' ' -f2- || echo "")
|
||||||
|
|
||||||
|
DISKS_JSON="[]"
|
||||||
|
DISKS_RAW=$(echo "$CONFIG" | grep -E '^(scsi|sata|virtio|ide)[0-9]+:' || true)
|
||||||
|
if [ -n "$DISKS_RAW" ]; then
|
||||||
|
DISKS_TMP=$(mktemp)
|
||||||
|
echo "$DISKS_RAW" | while IFS= read -r LINE; do
|
||||||
|
DEV=$(echo "$LINE" | cut -d: -f1)
|
||||||
|
SIZE=$(echo "$LINE" | grep -oP 'size=\K[^,]+' || echo "")
|
||||||
|
jq -n --arg device "$DEV" --arg size "$SIZE" '{device:$device, size:$size}'
|
||||||
|
done > "$DISKS_TMP"
|
||||||
|
DISKS_JSON=$(jq -s '.' "$DISKS_TMP")
|
||||||
|
rm -f "$DISKS_TMP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
IPCONFIG_LINE=$(echo "$CONFIG" | grep -E '^ipconfig0:' || true)
|
||||||
|
CFG_GW=$(echo "$IPCONFIG_LINE" | grep -oP 'gw=\K[0-9.]+' || echo "")
|
||||||
|
CFG_CIDR=$(echo "$IPCONFIG_LINE" | grep -oP 'ip=[0-9.]+/\K[0-9]+' || echo "")
|
||||||
|
|
||||||
|
IP=""
|
||||||
|
LIVE_CIDR=""
|
||||||
|
if [ "$STATUS" = "running" ]; then
|
||||||
|
AGENT_JSON=$(qm guest cmd "$VMID" network-get-interfaces 2>/dev/null || echo "")
|
||||||
|
if [ -n "$AGENT_JSON" ]; then
|
||||||
|
IP=$(echo "$AGENT_JSON" | grep -o '"ip-address":"[^"]*"' | grep -v '127.0.0.1' | cut -d'"' -f4 | head -1 || echo "")
|
||||||
|
LIVE_CIDR=$(echo "$AGENT_JSON" | grep -oP '"prefix":\K[0-9]+' | head -1 || echo "")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
GW="${CFG_GW:-}"
|
||||||
|
CIDR="${LIVE_CIDR:-$CFG_CIDR}"
|
||||||
|
NETMASK=""
|
||||||
|
[ -n "$CIDR" ] && NETMASK=$(cidr_to_netmask "$CIDR")
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--argjson vmid "$VMID" \
|
||||||
|
--arg type "vm" \
|
||||||
|
--arg hostname "$NAME" \
|
||||||
|
--arg status "$STATUS" \
|
||||||
|
--arg node "$(hostname)" \
|
||||||
|
--arg cores "${CORES:-null}" \
|
||||||
|
--arg memory_mb "${MEMORY:-null}" \
|
||||||
|
--argjson disks "$DISKS_JSON" \
|
||||||
|
--arg ip "$IP" \
|
||||||
|
--arg gateway "$GW" \
|
||||||
|
--arg netmask "$NETMASK" \
|
||||||
|
'{
|
||||||
|
vmid: $vmid,
|
||||||
|
type: $type,
|
||||||
|
hostname: $hostname,
|
||||||
|
status: $status,
|
||||||
|
proxmox_node: $node,
|
||||||
|
cores: (if $cores == "null" then null else ($cores|tonumber) end),
|
||||||
|
memory_mb: (if $memory_mb == "null" then null else ($memory_mb|tonumber) end),
|
||||||
|
disks: $disks,
|
||||||
|
ip: $ip,
|
||||||
|
gateway: $gateway,
|
||||||
|
netmask: $netmask
|
||||||
|
}' >> "$TMP_VM"
|
||||||
|
done
|
||||||
|
|
||||||
|
CONTAINERS_JSON=$(jq -s '.' "$TMP_LXC")
|
||||||
|
VMS_JSON=$(jq -s '.' "$TMP_VM")
|
||||||
|
|
||||||
|
# Prüfen, ob per --vmids/VMID_FILTER angeforderte VMIDs in Proxmox nicht existieren
|
||||||
|
MISSING_JSON="[]"
|
||||||
|
if [ -n "$VMID_FILTER" ]; then
|
||||||
|
ALL_KNOWN_VMIDS=$( (pct list | awk 'NR>1{print $1}'; qm list | awk 'NR>1{print $1}') | sort -u)
|
||||||
|
MISSING_TMP=$(mktemp)
|
||||||
|
IFS=',' read -ra REQUESTED_VMIDS <<< "$VMID_FILTER"
|
||||||
|
for rid in "${REQUESTED_VMIDS[@]}"; do
|
||||||
|
if ! echo "$ALL_KNOWN_VMIDS" | grep -qx "$rid"; then
|
||||||
|
jq -n --arg vmid "$rid" '{vmid: ($vmid|tonumber), reason: "VMID in Proxmox nicht gefunden (weder pct noch qm list)"}' >> "$MISSING_TMP"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -s "$MISSING_TMP" ]; then
|
||||||
|
MISSING_JSON=$(jq -s '.' "$MISSING_TMP")
|
||||||
|
fi
|
||||||
|
rm -f "$MISSING_TMP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PAYLOAD=$(jq -n \
|
||||||
|
--arg host "$(hostname)" \
|
||||||
|
--arg ts "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
|
||||||
|
--argjson containers "$CONTAINERS_JSON" \
|
||||||
|
--argjson vms "$VMS_JSON" \
|
||||||
|
--argjson missing "$MISSING_JSON" \
|
||||||
|
'{proxmox_host: $host, generated_at: $ts, containers: $containers, vms: $vms, missing_vmids: $missing}')
|
||||||
|
|
||||||
|
if [ "$PRINT_ONLY" = true ]; then
|
||||||
|
echo "$PAYLOAD"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$PAYLOAD" | jq '.' >&2
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = true ]; then
|
||||||
|
echo "Dry-Run: es wurde NICHTS an n8n gesendet." >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
HTTP_CODE=$(curl -s -o /tmp/n8n-response.json -w "%{http_code}" \
|
||||||
|
-X POST "$N8N_WEBHOOK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data "$PAYLOAD")
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||||
|
echo "OK: an n8n gesendet (HTTP $HTTP_CODE)" >&2
|
||||||
|
else
|
||||||
|
echo "FEHLER: n8n antwortete mit HTTP $HTTP_CODE" >&2
|
||||||
|
cat /tmp/n8n-response.json >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# proxmox-inventory.sh
|
||||||
|
#
|
||||||
|
# Liest alle LXC-Container und VMs auf diesem Proxmox-Host aus und gibt
|
||||||
|
# eine strukturierte Übersicht aus (VMID, Name, Status, Ressourcen, Netzwerk
|
||||||
|
# inkl. Subnetzmaske und Gateway).
|
||||||
|
#
|
||||||
|
# Ausführen als root auf dem Proxmox-Host:
|
||||||
|
# chmod +x proxmox-inventory.sh
|
||||||
|
# ./proxmox-inventory.sh
|
||||||
|
#
|
||||||
|
# Die Ausgabe kannst du 1:1 kopieren und zurückgeben, um die Outline-Doku
|
||||||
|
# aktualisieren zu lassen. Optional: Ausgabe in Datei umleiten:
|
||||||
|
# ./proxmox-inventory.sh > inventory.txt
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
separator() { printf '%s\n' "----------------------------------------------------------------------"; }
|
||||||
|
|
||||||
|
# Wandelt eine CIDR-Prefixlänge (z. B. 24) in eine Subnetzmaske (255.255.255.0) um.
|
||||||
|
cidr_to_netmask() {
|
||||||
|
local cidr="$1"
|
||||||
|
if ! [[ "$cidr" =~ ^[0-9]+$ ]] || [ "$cidr" -lt 0 ] || [ "$cidr" -gt 32 ]; then
|
||||||
|
echo "-"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local mask=0
|
||||||
|
if [ "$cidr" -gt 0 ]; then
|
||||||
|
mask=$(( 0xffffffff ^ ((1 << (32 - cidr)) - 1) ))
|
||||||
|
else
|
||||||
|
mask=0
|
||||||
|
fi
|
||||||
|
printf "%d.%d.%d.%d\n" \
|
||||||
|
"$(( (mask >> 24) & 255 ))" \
|
||||||
|
"$(( (mask >> 16) & 255 ))" \
|
||||||
|
"$(( (mask >> 8) & 255 ))" \
|
||||||
|
"$(( mask & 255 ))"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "########################################"
|
||||||
|
echo "# Proxmox Inventar - $(hostname) - $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
echo "########################################"
|
||||||
|
echo
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# LXC Container
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "=========================================="
|
||||||
|
echo " LXC CONTAINER"
|
||||||
|
echo "=========================================="
|
||||||
|
echo
|
||||||
|
|
||||||
|
for VMID in $(pct list | awk 'NR>1 {print $1}'); do
|
||||||
|
separator
|
||||||
|
echo "VMID: $VMID"
|
||||||
|
pct list | awk -v id="$VMID" '$1==id {print "Status: " $2; print "Name (pct list): " $3}'
|
||||||
|
|
||||||
|
CONFIG=$(pct config "$VMID" 2>/dev/null || true)
|
||||||
|
HOSTNAME=$(echo "$CONFIG" | grep -E '^hostname:' | cut -d' ' -f2- || echo "-")
|
||||||
|
CORES=$(echo "$CONFIG" | grep -E '^cores:' | cut -d' ' -f2- || echo "-")
|
||||||
|
MEMORY=$(echo "$CONFIG" | grep -E '^memory:' | cut -d' ' -f2- || echo "-")
|
||||||
|
SWAP=$(echo "$CONFIG" | grep -E '^swap:' | cut -d' ' -f2- || echo "-")
|
||||||
|
ROOTFS=$(echo "$CONFIG" | grep -E '^rootfs:' | cut -d' ' -f2- || echo "-")
|
||||||
|
echo "Hostname (config): $HOSTNAME"
|
||||||
|
echo "Cores: $CORES"
|
||||||
|
echo "Memory (MB): $MEMORY"
|
||||||
|
echo "Swap (MB): $SWAP"
|
||||||
|
echo "Rootfs/Storage: $ROOTFS"
|
||||||
|
|
||||||
|
echo "Netzwerk-Konfiguration:"
|
||||||
|
echo "$CONFIG" | grep -E '^net[0-9]+:' | while read -r LINE; do
|
||||||
|
echo " $LINE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Gateway und CIDR-Prefix aus der net0-Zeile der Config extrahieren (statische Konfiguration)
|
||||||
|
NET0_LINE=$(echo "$CONFIG" | grep -E '^net0:' || true)
|
||||||
|
CFG_GW=$(echo "$NET0_LINE" | grep -oP 'gw=\K[0-9.]+' || echo "")
|
||||||
|
CFG_CIDR=$(echo "$NET0_LINE" | grep -oP 'ip=[0-9.]+/\K[0-9]+' || echo "")
|
||||||
|
|
||||||
|
# Tatsächlich zugewiesene IP + Prefix (falls Container läuft und Tools installiert sind)
|
||||||
|
LIVE_GW=""
|
||||||
|
LIVE_CIDR=""
|
||||||
|
if pct status "$VMID" 2>/dev/null | grep -q running; then
|
||||||
|
IP=$(pct exec "$VMID" -- ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | paste -sd ', ' || echo "n/a")
|
||||||
|
echo "Live-IP(s): $IP"
|
||||||
|
LIVE_CIDR=$(pct exec "$VMID" -- ip -4 -o addr show scope global 2>/dev/null | head -1 | awk '{print $4}' | cut -d/ -f2 || echo "")
|
||||||
|
LIVE_GW=$(pct exec "$VMID" -- ip -4 route show default 2>/dev/null | awk '{print $3}' | head -1 || echo "")
|
||||||
|
else
|
||||||
|
echo "Live-IP(s): (Container gestoppt)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gateway: bevorzugt live ermittelt, sonst aus Config
|
||||||
|
GW="${LIVE_GW:-${CFG_GW:-"-"}}"
|
||||||
|
[ -z "$GW" ] && GW="-"
|
||||||
|
echo "Gateway: $GW"
|
||||||
|
|
||||||
|
# Subnetzmaske: bevorzugt live ermittelter Prefix, sonst Config-Prefix
|
||||||
|
CIDR="${LIVE_CIDR:-${CFG_CIDR:-""}}"
|
||||||
|
if [ -n "$CIDR" ]; then
|
||||||
|
NETMASK=$(cidr_to_netmask "$CIDR")
|
||||||
|
echo "Subnetzmaske: $NETMASK (/$CIDR)"
|
||||||
|
else
|
||||||
|
echo "Subnetzmaske: -"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=========================================="
|
||||||
|
echo " VIRTUELLE MASCHINEN (QEMU/KVM)"
|
||||||
|
echo "=========================================="
|
||||||
|
echo
|
||||||
|
|
||||||
|
for VMID in $(qm list | awk 'NR>1 {print $1}'); do
|
||||||
|
separator
|
||||||
|
echo "VMID: $VMID"
|
||||||
|
qm list | awk -v id="$VMID" '$1==id {print "Name (qm list): " $2; print "Status: " $3}'
|
||||||
|
|
||||||
|
CONFIG=$(qm config "$VMID" 2>/dev/null || true)
|
||||||
|
CORES=$(echo "$CONFIG" | grep -E '^cores:' | cut -d' ' -f2- || echo "-")
|
||||||
|
SOCKETS=$(echo "$CONFIG"| grep -E '^sockets:'| cut -d' ' -f2- || echo "-")
|
||||||
|
MEMORY=$(echo "$CONFIG" | grep -E '^memory:' | cut -d' ' -f2- || echo "-")
|
||||||
|
echo "Cores: $CORES"
|
||||||
|
echo "Sockets: $SOCKETS"
|
||||||
|
echo "Memory (MB): $MEMORY"
|
||||||
|
echo "Disks:"
|
||||||
|
echo "$CONFIG" | grep -E '^(scsi|sata|virtio|ide)[0-9]+:' | while read -r LINE; do
|
||||||
|
echo " $LINE"
|
||||||
|
done
|
||||||
|
echo "Netzwerk-Konfiguration:"
|
||||||
|
echo "$CONFIG" | grep -E '^net[0-9]+:' | while read -r LINE; do
|
||||||
|
echo " $LINE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cloud-Init-Netzwerkdaten (falls vorhanden): ipconfig0: ip=X.X.X.X/24,gw=Y.Y.Y.Y
|
||||||
|
IPCONFIG_LINE=$(echo "$CONFIG" | grep -E '^ipconfig0:' || true)
|
||||||
|
CFG_GW=$(echo "$IPCONFIG_LINE" | grep -oP 'gw=\K[0-9.]+' || echo "")
|
||||||
|
CFG_CIDR=$(echo "$IPCONFIG_LINE" | grep -oP 'ip=[0-9.]+/\K[0-9]+' || echo "")
|
||||||
|
|
||||||
|
LIVE_CIDR=""
|
||||||
|
# QEMU-Guest-Agent IP/Prefix, falls installiert & VM läuft
|
||||||
|
if qm status "$VMID" 2>/dev/null | grep -q running; then
|
||||||
|
AGENT_JSON=$(qm guest cmd "$VMID" network-get-interfaces 2>/dev/null || echo "")
|
||||||
|
IP=$(echo "$AGENT_JSON" \
|
||||||
|
| grep -o '"ip-address":"[^"]*"' \
|
||||||
|
| grep -v '127.0.0.1' \
|
||||||
|
| cut -d'"' -f4 \
|
||||||
|
| paste -sd ', ' || echo "")
|
||||||
|
if [ -n "$IP" ]; then
|
||||||
|
echo "Live-IP(s) via Guest-Agent: $IP"
|
||||||
|
# Prefix der ersten nicht-lokalen Adresse ermitteln
|
||||||
|
LIVE_CIDR=$(echo "$AGENT_JSON" | grep -oP '"prefix":\K[0-9]+' | head -1 || echo "")
|
||||||
|
else
|
||||||
|
echo "Live-IP(s) via Guest-Agent: n/a (kein Guest-Agent?)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Live-IP(s): (VM gestoppt)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gateway: nur über Cloud-Init-Config ermittelbar (Guest-Agent liefert kein Gateway)
|
||||||
|
GW="${CFG_GW:-"-"}"
|
||||||
|
[ -z "$GW" ] && GW="-"
|
||||||
|
echo "Gateway: $GW"
|
||||||
|
|
||||||
|
CIDR="${LIVE_CIDR:-${CFG_CIDR:-""}}"
|
||||||
|
if [ -n "$CIDR" ]; then
|
||||||
|
NETMASK=$(cidr_to_netmask "$CIDR")
|
||||||
|
echo "Subnetzmaske: $NETMASK (/$CIDR)"
|
||||||
|
else
|
||||||
|
echo "Subnetzmaske: - (nicht ermittelbar ohne Cloud-Init-Config oder Guest-Agent-Prefix)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "########################################"
|
||||||
|
echo "# Ende der Ausgabe"
|
||||||
|
echo "########################################"
|
||||||
Reference in New Issue
Block a user