OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

host_sync_settings.php

15 KB
<?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>