- n8n workflow: apply all field changes to full document text in one pass, send single documents.update per container/VM instead of multiple patch calls (fixed race condition where parallel patches to the same doc could partially overwrite each other, e.g. RAM update lost when Swap changed simultaneously) - n8n workflow: add HTTP Request Doc Info (documents.info) node to fetch fresh document content by ID instead of relying on the search index snapshot, which can lag slightly behind - n8n workflow: add Aggregate node after documents.update so the Loop Over Items (Split In Batches) counter stays correct regardless of how many fields changed - proxmox-inventory-json.sh: explicit PATH export so pct/qm are found under cron's restricted PATH (fixes empty containers/vms payload in production cron runs while manual testing looked fine) - README: document all of the above + known limitation with Outline table column width metadata not being touched by content-only sync
247 lines
14 KiB
JSON
247 lines
14 KiB
JSON
{
|
|
"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\nconst items = [...containers, ...vms];\n\n// Sicherheitsnetz: leere Daten nicht stillschweigend abbrechen lassen\nif (items.length === 0) {\n staticData.summary.push({\n vmid: '-',\n name: '-',\n changes: ['⚠️ Keine Container/VMs im Payload - Script-Fehler auf dem Proxmox-Host? (z.B. PATH-Problem in Cron)']\n });\n return [{ json: { __empty_run: true } }];\n}\n\nreturn items.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;\nlet text = doc.text || '';\n\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 text = text.slice(0, match.index) + match[1] + desiredValue + match[3] + text.slice(match.index + match[0].length);\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 (changedFields.length === 0) {\n return [{ json: { skip: true } }];\n}\n\nreturn [{ json: { skip: false, documentId: doc.id, fullText: text } }];"
|
|
},
|
|
"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,
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{ "name": "id", "value": "={{$json.documentId}}" },
|
|
{ "name": "text", "value": "={{$json.fullText}}" }
|
|
]
|
|
},
|
|
"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"
|
|
},
|
|
{
|
|
"parameters": { "aggregate": "aggregateAllItemData", "options": {} },
|
|
"type": "n8n-nodes-base.aggregate",
|
|
"typeVersion": 1,
|
|
"position": [1488, -176],
|
|
"id": "c0568872-e56f-472e-8276-2d0420d8c51f",
|
|
"name": "Aggregate"
|
|
}
|
|
],
|
|
"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": "Aggregate", "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 }]] },
|
|
"Aggregate": { "main": [[{ "node": "Loop Over Items", "type": "main", "index": 0 }]] }
|
|
}
|
|
}
|