Files
Ceph-Calculator/app/templates/index.html

488 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="de" x-data="{ darkMode: localStorage.getItem('darkMode') === 'true' || (!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches) }"
:class="{ 'dark': darkMode }"
class="transition-colors duration-500 ease-in-out">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Ceph Max Storage Rechner</title>
<!-- HTMX für interaktive Elemente -->
<script src="https://unpkg.com/htmx.org@1.9.4"></script>
<!-- Tailwind CSS für modernes Design -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
dark: {
bg: '#1a1a1a',
card: '#2d2d2d',
border: '#404040',
text: '#e5e5e5',
input: '#404040',
hover: '#505050'
}
},
transitionProperty: {
'all': 'all',
'colors': 'color, background-color, border-color, text-decoration-color, fill, stroke',
'opacity': 'opacity',
'shadow': 'box-shadow',
'transform': 'transform',
},
transitionDuration: {
'200': '200ms',
'300': '300ms',
'500': '500ms',
'700': '700ms',
},
transitionTimingFunction: {
'ease-in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
}
}
}
}
</script>
<!-- Alpine.js für reaktive Benutzeroberfläche -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.0/dist/cdn.min.js"></script>
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
</head>
<body class="bg-gray-100 dark:bg-dark-bg min-h-screen transition-all duration-500 ease-in-out" x-data="cephCalculator()">
<div class="w-full px-4 py-8 bg-gray-100 dark:bg-dark-bg min-h-screen transition-all duration-500 ease-in-out">
<div class="max-w-7xl mx-auto">
<!-- Theme Toggle -->
<div class="fixed top-4 right-4 z-50">
<button @click="darkMode = !darkMode; localStorage.setItem('darkMode', darkMode)"
class="p-2 rounded-lg bg-white dark:bg-dark-card shadow-lg hover:bg-gray-100 dark:hover:bg-dark-hover transition-all duration-500 ease-in-out transform hover:scale-105">
<!-- Sonne Icon für Light Mode -->
<svg x-show="!darkMode" class="w-6 h-6 text-gray-800 transition-opacity duration-500 ease-in-out" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<!-- Mond Icon für Dark Mode -->
<svg x-show="darkMode" class="w-6 h-6 text-gray-200 transition-opacity duration-500 ease-in-out" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</button>
</div>
<header class="mb-8">
<h1 class="text-3xl font-bold text-center text-blue-700 dark:text-blue-400">Ceph Max Storage Kapazitätsrechner</h1>
<p class="text-gray-600 dark:text-gray-400 text-center mt-2">Berechnen Sie die maximale Speichernutzung Ihres Ceph-Clusters</p>
</header>
<main class="bg-white dark:bg-dark-card rounded-lg shadow-lg p-6 lg:p-8 transition-colors duration-200">
<!-- Replikationstyp -->
<div class="mb-6">
<h2 class="text-xl font-semibold mb-3 text-gray-800 dark:text-dark-text">Replikationstyp</h2>
<div class="flex flex-wrap gap-4">
<label class="flex items-center">
<input type="radio" name="replication_type" value="replication"
x-model="replicationType" class="mr-2 h-5 w-5">
<span class="text-gray-700 dark:text-dark-text">Replikation</span>
</label>
<label class="flex items-center">
<input type="radio" name="replication_type" value="erasure_coding"
x-model="replicationType" class="mr-2 h-5 w-5">
<span class="text-gray-700 dark:text-dark-text">Erasure Coding</span>
</label>
</div>
</div>
<!-- Replikationskonfiguration -->
<div class="mb-6" x-show="replicationType === 'replication'">
<h2 class="text-xl font-semibold mb-3 text-gray-800 dark:text-dark-text">Replikationskonfiguration</h2>
<div class="flex items-center mb-3">
<label class="w-32 text-gray-700 dark:text-dark-text">Anzahl Replikate:</label>
<div class="relative group">
<input type="number" x-model="replicas" min="1" max="10"
class="border rounded p-2 w-20 text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
<div class="absolute hidden group-hover:block bg-gray-800 text-white p-2 rounded text-sm">
Anzahl der Kopien, die von jedem Datenblock erstellt werden
</div>
</div>
</div>
<div class="flex items-center mb-3">
<label class="w-32 text-gray-700 dark:text-dark-text">Min. Replikate (min_size):</label>
<input type="number" x-model="minSize" min="1" :max="replicas"
class="border rounded p-2 w-20 text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">Minimale Anzahl von Kopien für I/O-Operationen</span>
</div>
<div class="flex items-center mb-3">
<label class="w-32 text-gray-700 dark:text-dark-text">Speichereinheit:</label>
<select x-model="storageUnit" class="border rounded p-2 bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
<option value="GB">GB</option>
<option value="TB">TB</option>
</select>
</div>
</div>
<!-- Erasure Coding Konfiguration -->
<div class="mb-6" x-show="replicationType === 'erasure_coding'">
<h2 class="text-xl font-semibold mb-3 text-gray-800 dark:text-dark-text">Erasure Coding Konfiguration</h2>
<div class="flex flex-wrap gap-6">
<div class="flex items-center">
<label class="w-32 text-gray-700 dark:text-dark-text">Datenchunks (k):</label>
<input type="number" x-model="k" min="1" max="20"
class="border rounded p-2 w-20 text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
</div>
<div class="flex items-center">
<label class="w-32 text-gray-700 dark:text-dark-text">Codierungschunks (m):</label>
<input type="number" x-model="m" min="1" max="10"
class="border rounded p-2 w-20 text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
</div>
</div>
<div class="flex items-center mt-4">
<label class="w-32 text-gray-700 dark:text-dark-text">Speichereinheit:</label>
<select x-model="storageUnit" class="border rounded p-2 bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
<option value="GB">GB</option>
<option value="TB">TB</option>
</select>
</div>
</div>
<!-- Nodes und OSDs -->
<div class="mb-6">
<h2 class="text-xl font-semibold mb-3 text-gray-800 dark:text-dark-text">Nodes und OSDs</h2>
<template x-for="(node, nodeIndex) in nodes" :key="nodeIndex">
<div class="border p-4 rounded-lg mb-4 bg-gray-50 dark:bg-dark-input dark:border-dark-border">
<div class="flex flex-wrap items-center gap-4 mb-3">
<h3 class="font-medium dark:text-dark-text" x-text="`Node ${nodeIndex + 1}`"></h3>
<button @click="removeNode(nodeIndex)"
class="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600 ml-auto">
Entfernen
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
<label class="block text-gray-700 dark:text-dark-text mb-2">OSD Anzahl:</label>
<input type="number" x-model="node.osd_count" min="1"
class="border rounded p-2 w-full text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
</div>
<div class="form-group">
<label class="block text-gray-700 dark:text-dark-text mb-2">OSD Größe:</label>
<div class="flex items-center">
<input type="number" x-model="node.osd_size_gb" min="1"
class="border rounded p-2 w-full text-center bg-white dark:bg-dark-input dark:border-dark-border dark:text-dark-text">
<span class="ml-2 text-gray-700 dark:text-dark-text" x-text="storageUnit"></span>
</div>
</div>
</div>
</div>
</template>
<button @click="addNode()"
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
Node hinzufügen
</button>
</div>
<!-- Berechnungsbutton -->
<div class="flex justify-center mt-8 space-x-4">
<button @click="calculateCapacity()"
class="bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold
shadow-md hover:bg-blue-700 transition duration-300"
:disabled="isCalculating">
<span x-show="!isCalculating">Kapazität berechnen</span>
<span x-show="isCalculating" class="flex items-center">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Berechne...
</span>
</button>
<button @click="exportConfig()"
class="bg-green-600 text-white px-6 py-3 rounded-lg font-semibold
shadow-md hover:bg-green-700 transition duration-300">
Konfiguration exportieren
</button>
<label class="bg-purple-600 text-white px-6 py-3 rounded-lg font-semibold
shadow-md hover:bg-purple-700 transition duration-300 cursor-pointer">
Konfiguration importieren
<input type="file" @change="importConfig($event)" class="hidden" accept=".json">
</label>
<button @click="generatePDF()"
class="bg-red-600 text-white px-6 py-3 rounded-lg font-semibold
shadow-md hover:bg-red-700 transition duration-300"
:disabled="!result.raw_total">
PDF Report generieren
</button>
</div>
<!-- Ergebnisbereich -->
<div class="mt-8" x-show="result.raw_total > 0">
<h2 class="text-xl font-semibold mb-4 text-gray-800 dark:text-dark-text">Ergebnis</h2>
<div class="bg-blue-50 dark:bg-dark-input p-6 rounded-lg shadow border border-blue-200 dark:border-dark-border">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="p-3">
<p class="text-gray-600 dark:text-gray-400 mb-1">Gesamt-Rohspeicher:</p>
<p class="text-2xl font-bold text-gray-800 dark:text-dark-text">
<span x-text="storageUnit === 'TB' ? `${(result.raw_total / 1024).toFixed(2)} TB` : `${result.raw_total} GB`"></span>
</p>
</div>
<div class="p-3 bg-green-50 dark:bg-dark-input rounded-lg border border-green-100 dark:border-dark-border">
<p class="text-gray-600 dark:text-gray-400 mb-1">Maximale empfohlene Speichernutzung:</p>
<p class="text-2xl font-bold text-green-600 dark:text-green-400" x-text="`${result.max_usage_percent}%`"></p>
<p class="text-xl font-semibold text-gray-800 dark:text-dark-text mt-2">
<span x-text="storageUnit === 'TB' ? `${result.max_usage_tb} TB` : `${result.max_usage_gb} GB`"></span>
</p>
</div>
</div>
<!-- Ausfalltoleranz-Informationen -->
<div class="mt-4 border-t border-blue-200 dark:border-dark-border pt-4">
<h3 class="text-lg font-semibold mb-3 text-gray-800 dark:text-dark-text">Ausfalltoleranz-Analyse</h3>
<!-- Node-Ausfalltoleranz -->
<div class="p-3 rounded-lg mb-3"
:class="result.node_failure_tolerance ? 'bg-green-50 dark:bg-dark-input border border-green-200 dark:border-dark-border' : 'bg-red-50 dark:bg-dark-input border border-red-200 dark:border-dark-border'">
<div class="flex items-center mb-2">
<span class="font-semibold text-gray-800 dark:text-dark-text mr-2">Node-Ausfalltoleranz:</span>
<span class="px-2 py-1 rounded text-xs font-semibold"
:class="result.node_failure_tolerance ? 'bg-green-600 text-white' : 'bg-red-600 text-white'"
x-text="result.node_failure_tolerance ? 'Vorhanden' : 'Nicht vorhanden'"></span>
<span x-show="result.multi_failure_tolerance" class="ml-2 px-2 py-1 rounded text-xs font-semibold bg-blue-600 text-white"
x-text="`Multi-Node (${result.max_failure_nodes})`"></span>
</div>
<p class="text-sm dark:text-gray-300" x-text="result.node_failure_info"></p>
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
<div>
<span class="text-gray-600 dark:text-gray-400">Größter Node:</span>
<span class="font-medium ml-1 dark:text-dark-text" x-text="storageUnit === 'TB' ? `${(result.largest_node_gb / 1024).toFixed(2)} TB` : `${result.largest_node_gb} GB`"></span>
</div>
<div>
<span class="text-gray-600 dark:text-gray-400">Rohspeicher nach Nodeausfall:</span>
<span class="font-medium ml-1 dark:text-dark-text" x-text="storageUnit === 'TB' ? `${(result.raw_after_failure_gb / 1024).toFixed(2)} TB` : `${result.raw_after_failure_gb} GB`"></span>
</div>
<div>
<span class="text-gray-600 dark:text-gray-400">Nutzbarer Speicher nach Ausfall:</span>
<span class="font-medium ml-1 dark:text-dark-text" x-text="storageUnit === 'TB' ? `${(result.usable_after_failure_gb / 1024).toFixed(2)} TB` : `${result.usable_after_failure_gb} GB`"></span>
</div>
<div x-show="result.multi_failure_tolerance || result.max_failure_nodes > 1">
<span class="text-gray-600 dark:text-gray-400">Nutzbarer Speicher nach max. Ausfällen:</span>
<span class="font-medium ml-1 dark:text-dark-text" x-text="storageUnit === 'TB' ? `${(result.usable_after_max_failures_gb / 1024).toFixed(2)} TB` : `${result.usable_after_max_failures_gb} GB`"></span>
</div>
</div>
<!-- Min-Size Info -->
<div class="mt-3 p-2 bg-blue-100 dark:bg-dark-input rounded text-sm" x-show="result.min_size">
<p class="dark:text-gray-300">
<span class="font-semibold">Min-Size:</span>
<span x-text="result.min_size"></span> -
Diese Einstellung bestimmt die minimale Anzahl von Replikaten für I/O-Operationen.
Der Cluster kann bis zu <span x-text="result.max_failure_nodes"></span> Nodes verlieren,
bevor die Daten unzugänglich werden.
</p>
</div>
</div>
<!-- OSD-Ausfalltoleranz -->
<div class="p-3 rounded-lg"
:class="result.osd_failure_tolerance ? 'bg-green-50 dark:bg-dark-input border border-green-200 dark:border-dark-border' : 'bg-yellow-50 dark:bg-dark-input border border-yellow-200 dark:border-dark-border'">
<div class="flex items-center mb-2">
<span class="font-semibold text-gray-800 dark:text-dark-text mr-2">OSD-Ausfalltoleranz:</span>
<span class="px-2 py-1 rounded text-xs font-semibold"
:class="result.osd_failure_tolerance ? 'bg-green-600 text-white' : 'bg-yellow-600 text-white'"
x-text="result.osd_failure_tolerance ? 'Vorhanden' : 'Eingeschränkt'"></span>
</div>
<p class="text-sm dark:text-gray-300" x-text="result.osd_failure_info"></p>
</div>
<!-- Empfehlungen basierend auf Ceph-Dokumentation -->
<div class="mt-4 p-3 bg-blue-50 dark:bg-dark-input rounded-lg border border-blue-200 dark:border-dark-border">
<h4 class="font-semibold text-gray-800 dark:text-dark-text mb-2">Empfehlungen gemäß Ceph-Dokumentation:</h4>
<ul class="list-disc list-inside text-sm space-y-1 dark:text-gray-300">
<li>Stelle sicher, dass nach Nodeausfällen genügend Kapazität vorhanden ist, um die 'full ratio' nicht zu erreichen.</li>
<li>Bei Replikation sollte die Anzahl der Nodes größer sein als der Replikationsfaktor.</li>
<li>Bei Erasure Coding sollte die Anzahl der Nodes größer sein als k+m (Datenchunks + Codierungschunks).</li>
<li>Die Performance bei der Wiederherstellung hängt von der Netzwerk- und Speichergeschwindigkeit ab.</li>
<li>Beachte, dass nach einem Nodeausfall das Wiederausbalancieren des Clusters länger dauert, je größer der ausgefallene Node ist.</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<footer class="mt-8 text-center text-gray-500 dark:text-gray-400 text-sm">
<p>Basierend auf Ceph-Dokumentation | Ergebnisse dienen nur als Richtwert</p>
</footer>
</div>
</div>
<script>
function cephCalculator() {
return {
replicationType: 'replication',
replicas: 3,
minSize: 2,
k: 4,
m: 2,
storageUnit: 'TB',
nodes: [
{
osd_count: 4,
osd_size_gb: 1000
}
],
result: {
max_usage_percent: 0,
max_usage_gb: 0,
max_usage_tb: 0,
raw_total: 0
},
isCalculating: false,
init() {
const savedConfig = localStorage.getItem('lastConfig');
if (savedConfig) {
const config = JSON.parse(savedConfig);
Object.assign(this, config);
}
},
addNode() {
this.nodes.push({
osd_count: 4,
osd_size_gb: this.storageUnit === 'TB' ? 1000 : 1
});
},
removeNode(nodeIndex) {
this.nodes.splice(nodeIndex, 1);
if (this.nodes.length === 0) {
this.addNode();
}
},
getOsdSizeInGB(size) {
return this.storageUnit === 'TB' ? size * 1024 : size;
},
calculateCapacity() {
const data = {
replication_type: this.replicationType,
replicas: parseInt(this.replicas),
k: parseInt(this.k),
m: parseInt(this.m),
nodes: this.nodes.map(node => ({
osd_count: parseInt(node.osd_count),
osd_size_gb: this.getOsdSizeInGB(parseFloat(node.osd_size_gb))
})),
min_size: parseInt(this.minSize),
storage_unit: this.storageUnit
};
this.isCalculating = true;
fetch('/calculate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
this.result = data;
})
.catch(error => {
console.error('Fehler bei der Berechnung:', error);
alert('Es ist ein Fehler bei der Berechnung aufgetreten. Bitte versuchen Sie es erneut.');
})
.finally(() => {
this.isCalculating = false;
});
},
exportConfig() {
const config = {
replicationType: this.replicationType,
replicas: this.replicas,
minSize: this.minSize,
k: this.k,
m: this.m,
storageUnit: this.storageUnit,
nodes: this.nodes,
result: this.result
};
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ceph-config.json';
a.click();
URL.revokeObjectURL(url);
},
importConfig(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const config = JSON.parse(e.target.result);
Object.assign(this, config);
// Berechne automatisch nach dem Import
this.calculateCapacity();
} catch (error) {
alert('Fehler beim Importieren der Konfiguration. Bitte überprüfen Sie das Dateiformat.');
}
};
reader.readAsText(file);
},
generatePDF() {
if (!this.result.raw_total) {
alert('Bitte führen Sie zuerst eine Berechnung durch.');
return;
}
const data = {
replication_type: this.replicationType,
replicas: this.replicas,
min_size: this.minSize,
k: this.k,
m: this.m,
storage_unit: this.storageUnit,
nodes: this.nodes,
result: this.result
};
fetch('/generate-pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
},
body: JSON.stringify(data)
})
.then(response => response.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ceph-report.pdf';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
})
.catch(error => {
console.error('Fehler bei der PDF-Generierung:', error);
alert('Es ist ein Fehler bei der PDF-Generierung aufgetreten. Bitte versuchen Sie es erneut.');
});
}
};
}
</script>
</body>
</html>