Quelltext des aktuellen Standes
<?php
$host = $host ?? [];
$hasUrl = !empty($host['hypervisor_url'] ?? '');
$hasUser = !empty($host['hypervisor_user'] ?? '');
$hasToken = !empty($host['hypervisor_token'] ?? '');
$hasPass = !empty($host['hypervisor_password_enc'] ?? '');
// Sicher voreingestellt: bei noch nicht konfigurierten Hosts ist die
// Zertifikatsprüfung standardmäßig aktiv; konfigurierte Hosts behalten ihren Wert.
$verifySsl = $hasUrl ? !empty($host['hypervisor_verify_ssl']) : true;
$syncStatus = $host['sync_status'] ?? 'never';
$isActive = $hasUrl && ($hasUser || $hasToken);
$hypLabels = [
'vmware_esxi' => 'VMware vSphere / ESXi',
'proxmox' => 'Proxmox VE',
'hyper_v' => 'Microsoft Hyper-V',
'kvm' => 'KVM / QEMU',
'xen' => 'Xen',
'other' => 'Sonstige',
];
$hyp = $host['hypervisor'] ?? 'proxmox';
$urlPlaceholders = [
'proxmox' => 'https://192.168.1.10:8006',
'vmware_esxi' => 'https://vcenter.example.com',
'hyper_v' => 'http://hyperv-server:5985/wsman',
'kvm' => 'http://kvm-host:16509',
'xen' => 'https://xen-host',
'other' => 'https://hypervisor.example.com',
];
$urlHelp = [
'proxmox' => 'Proxmox VE REST-API. Port 8006 wird automatisch ergänzt falls kein Port angegeben.',
'vmware_esxi' => 'vCenter/ESXi REST-API. Benötigt ESXi/vCenter 6.5 oder neuer. ESXi 6.0 (nur SOAP) wird nicht unterstützt.',
'hyper_v' => 'WinRM-Endpunkt des Hyper-V-Hosts. HTTP Port 5985 oder HTTPS Port 5986.',
'kvm' => 'Libvirt REST-API oder benutzerdefinierter Endpunkt.',
'xen' => 'XenServer/XCP-ng API-URL.',
'other' => 'API-URL des Hypervisors.',
];
$tokenPlaceholders = [
'proxmox' => 'user@pam!tokenid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
'vmware_esxi' => 'Session-Token wird automatisch erzeugt',
'hyper_v' => 'Kein API-Token – Passwort-Authentifizierung verwenden',
'other' => 'API-Token',
];
$tokenHelp = [
'proxmox' => 'Proxmox API-Token im Format "user@realm!tokenid=secret". Wenn gesetzt, wird kein Passwort benötigt.',
'vmware_esxi' => 'VMware authentifiziert per Benutzername/Passwort und erstellt einen Session-Token automatisch.',
'hyper_v' => 'Hyper-V nutzt WinRM mit Benutzername und Passwort. API-Token wird ignoriert.',
'other' => 'Optionaler API-Token für die Authentifizierung.',
];
$this->layout('layouts/admin', [
'page_title' => $page_title,
'current_admin_page' => 'vm-hypervisor',
]);
?>
<div class="settings-page">
<!-- Breadcrumb -->
<div style="margin-bottom:1.25rem;">
<a href="/vms/hosts/<?= $host['id'] ?>" style="font-size:.82rem;color:var(--gray-500);text-decoration:none;">
← Zurück zu <?= $this->e($host['name']) ?>
</a>
</div>
<form action="/admin/vms/hosts/<?= $host['id'] ?>/sync-settings" method="POST">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<section class="settings-card">
<div class="settings-card-header">
<span class="settings-card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" width="24" height="24"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
</span>
<div>
<h2><?= $this->t('Hypervisor-API · {name}', ['name' => $this->e($host['name'])]) ?></h2>
<p class="settings-card-desc">
<?= $this->t('Konfiguriere die API-Verbindung für <strong>{hypervisor}</strong>. Zugangsdaten werden AES-256-GCM verschlüsselt gespeichert.', ['hypervisor' => $this->e($hypLabels[$host['hypervisor'] ?? ''] ?? ($host['hypervisor'] ?? '—'))]) ?>
</p>
</div>
</div>
<div class="settings-card-body">
<!-- Status-Banner -->
<?php if ($isActive && $syncStatus === 'ok'): ?>
<div class="vhs-banner vhs-banner-ok">
✓ <?= $this->t('API konfiguriert und zuletzt erfolgreich synchronisiert.') ?>
<?php if ($host['last_sync_at'] ?? null): ?>
Letzter Sync: <?= substr($host['last_sync_at'], 0, 16) ?>
<?php endif; ?>
— <a href="/vms/hosts/<?= $host['id'] ?>"><?= $this->te('Host anzeigen ↗') ?></a>
</div>
<?php elseif ($isActive && $syncStatus === 'error'): ?>
<div class="vhs-banner vhs-banner-warn">
⚠ <?= $this->t('API konfiguriert, aber letzter Sync fehlgeschlagen.') ?>
<?php if (!empty($host['sync_error'])): ?>
<br><code style="font-size:.78rem;"><?= $this->e($host['sync_error']) ?></code>
<?php endif; ?>
</div>
<?php elseif ($isActive): ?>
<div class="vhs-banner vhs-banner-info">
<?= $this->te('ℹ API konfiguriert, noch nie synchronisiert. Speichere die Einstellungen und teste die Verbindung.') ?>
</div>
<?php else: ?>
<div class="vhs-banner vhs-banner-neutral">
<?= $this->te('Noch keine API-Verbindung eingerichtet. Felder ausfüllen und speichern.') ?>
</div>
<?php endif; ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.25rem;max-width:720px;">
<!-- API-URL -->
<div class="form-group" style="grid-column:1/-1;">
<label for="vhs_url">API-URL</label>
<input type="url" id="vhs_url" name="hypervisor_url" class="form-control"
value="<?= $this->e($host['hypervisor_url'] ?? '') ?>"
placeholder="<?= $this->e($urlPlaceholders[$hyp] ?? 'https://hypervisor.example.com') ?>">
<small class="form-hint"><?= $this->e($urlHelp[$hyp] ?? '') ?></small>
</div>
<!-- Benutzername -->
<div class="form-group">
<label for="vhs_user"><?= $this->te('Benutzername / User') ?></label>
<input type="text" id="vhs_user" name="hypervisor_user" class="form-control"
value="<?= $this->e($host['hypervisor_user'] ?? '') ?>"
placeholder="root@pam" autocomplete="off">
</div>
<!-- Passwort -->
<div class="form-group">
<label for="vhs_pass"><?= $this->te('Passwort') ?></label>
<input type="password" id="vhs_pass" name="hypervisor_password" class="form-control"
placeholder="<?= $hasPass ? '●●●●●●●● (gesetzt)' : 'Passwort eingeben' ?>"
autocomplete="new-password">
<small class="form-hint"><?= $this->te('Leer lassen, um das bestehende Passwort beizubehalten.') ?></small>
</div>
<!-- API-Token -->
<div class="form-group" style="grid-column:1/-1;">
<label for="vhs_token"><?= $this->te('API-Token') ?> <span style="font-size:.75rem;color:var(--gray-500);font-weight:400;"><?= $this->te('(optional, überschreibt Passwort-Auth)') ?></span></label>
<input type="password" id="vhs_token" name="hypervisor_token" class="form-control"
placeholder="<?= $hasToken ? '●●●●●●●● (gesetzt)' : $this->e($tokenPlaceholders[$hyp] ?? 'API-Token') ?>"
autocomplete="new-password">
<small class="form-hint"><?= $this->e($tokenHelp[$hyp] ?? '') ?></small>
</div>
<!-- Node-ID -->
<div class="form-group">
<label for="vhs_node"><?= $this->te('Node-ID / Hostname') ?></label>
<input type="text" id="vhs_node" name="hypervisor_node_id" class="form-control"
value="<?= $this->e($host['hypervisor_node_id'] ?? '') ?>"
placeholder="<?= $this->te('z.B. pve (Proxmox) oder localhost (Hyper-V)') ?>">
<small class="form-hint"><?= $this->te('Nur bei Proxmox und Hyper-V erforderlich.') ?></small>
</div>
<!-- SSL -->
<div class="form-group" style="display:flex;align-items:center;margin-top:1.5rem;">
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;font-weight:400;margin:0;">
<input type="checkbox" name="hypervisor_verify_ssl" value="1" <?= $verifySsl ? 'checked' : '' ?>>
<span><?= $this->te('SSL-Zertifikat überprüfen') ?></span>
</label>
<small class="form-hint" style="margin-left:.5rem;"><?= $this->te('(deaktivieren bei self-signed Zertifikat)') ?></small>
</div>
</div>
<!-- Verbindungstest + Sync -->
<div style="margin-top:1.5rem;display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;">
<button type="button" class="btn btn-secondary" id="vhs-btn-test">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:.35rem;vertical-align:middle;"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
<?= $this->te('Verbindung testen') ?>
</button>
<button type="button" class="btn btn-info" id="vhs-btn-sync">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:.35rem;vertical-align:middle;"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
<?= $this->te('Jetzt synchronisieren') ?>
</button>
<span id="vhs-result" style="font-size:.83rem;"></span>
</div>
</div>
</section>
<div class="form-actions">
<button type="submit" class="btn btn-primary"><?= $this->te('Einstellungen speichern') ?></button>
<a href="/vms/hosts/<?= $host['id'] ?>" class="btn btn-default" style="margin-left:.5rem;"><?= $this->te('Abbrechen') ?></a>
</div>
</form>
<div style="margin-top:.5rem;font-size:.82rem;color:var(--gray-400);">
<a href="/admin/vms/setup-guide" style="color:var(--color-primary);text-decoration:none;font-weight:500;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13" style="vertical-align:middle;margin-right:.25rem;"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
<?= $this->t('Zur Einrichtungsanleitung für {hypervisor} →', ['hypervisor' => $this->e($hypLabels[$hyp] ?? $hyp)]) ?>
</a>
</div>
</div>
<style>
.vhs-banner {
padding:.65rem 1rem; border-radius:0.25rem; font-size:.82rem; margin-bottom:1.25rem;
line-height:1.5;
}
.vhs-banner-ok { background:var(--green-50); border:1px solid var(--green-200); color:var(--green-800); }
.vhs-banner-warn { background:var(--red-50); border:1px solid var(--red-200); color:var(--red-800); }
.vhs-banner-info { background:var(--blue-50); border:1px solid var(--blue-200); color:var(--blue-800); }
.vhs-banner-neutral { background:var(--gray-50); border:1px solid var(--gray-200); color:var(--gray-500); }
.vhs-banner a { color:inherit; font-weight:600; }
</style>
<script>
(function () {
var hostId = <?= (int)$host['id'] ?>;
var csrf = <?= json_encode($this->csrf()) ?>;
var resultEl = document.getElementById('vhs-result');
var formDirty = false;
// Formularänderungen erkennen
document.querySelector('form').addEventListener('input', function () { formDirty = true; });
function apiPost(path, btn, loadingText, cb) {
if (formDirty) {
resultEl.style.color = '#b45309';
resultEl.textContent = '⚠ Einstellungen zuerst speichern, sonst werden die alten Zugangsdaten verwendet.';
return;
}
var orig = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = loadingText;
resultEl.textContent = '';
fetch(path, {
method: 'POST',
headers: {'X-CSRF-Token': csrf, 'Content-Type': 'application/json'},
body: JSON.stringify({_csrf: csrf})
})
.then(function (r) { return r.json(); })
.then(function (d) { btn.disabled = false; btn.innerHTML = orig; cb(d); })
.catch(function (e) {
btn.disabled = false; btn.innerHTML = orig;
resultEl.style.color = '#b91c1c';
resultEl.textContent = '✗ Netzwerkfehler: ' + e.message;
});
}
var btnTest = document.getElementById('vhs-btn-test');
if (btnTest) {
btnTest.addEventListener('click', function () {
apiPost('/admin/vms/hosts/' + hostId + '/admin-test', btnTest, '⏳ Teste…', function (d) {
if (d.ok) {
resultEl.style.color = '#15803d';
resultEl.textContent = '✓ Verbindung erfolgreich!' + (d.version ? ' Version: ' + d.version : '');
} else {
resultEl.style.color = '#b91c1c';
resultEl.textContent = '✗ ' + (d.error || 'Verbindung fehlgeschlagen');
}
});
});
}
var btnSync = document.getElementById('vhs-btn-sync');
if (btnSync) {
btnSync.addEventListener('click', function () {
apiPost('/admin/vms/hosts/' + hostId + '/admin-sync', btnSync, '⏳ Synchronisiere…', function (d) {
if (d.ok) {
resultEl.style.color = '#15803d';
resultEl.textContent = '✓ Sync: ' + d.vms_synced + ' VMs ('
+ d.vms_created + ' neu, ' + d.vms_updated + ' aktualisiert)'
+ (d.vms_missing ? ', ' + d.vms_missing + ' nicht mehr gefunden' : '')
+ (d.snapshots ? ', ' + d.snapshots + ' Snapshots' : '')
+ (d.datastores ? ', ' + d.datastores + ' Speicherorte' : '')
+ (d.warnungen && d.warnungen.length ? ' — ' + d.warnungen.join('; ') : '');
setTimeout(function () { location.reload(); }, 1800);
} else {
resultEl.style.color = '#b91c1c';
resultEl.textContent = '✗ ' + (d.error || 'Sync fehlgeschlagen');
}
});
});
}
}());
</script>