Aktualisiere README und Code für den Ceph Max Storage Rechner: Übersetzung ins Englische, Verbesserung der Fehlerbehandlung in den Routen, Optimierung der PDF-Generierung und Anpassung der Benutzeroberfläche. Entferne veraltete JavaScript-Datei und aktualisiere die Struktur der Templates für bessere Lesbarkeit und Benutzerfreundlichkeit.

This commit is contained in:
Samuel Müller
2025-03-26 11:15:48 +01:00
parent 5359359531
commit 9388e576be
11 changed files with 727 additions and 761 deletions

112
README.md
View File

@ -1,96 +1,96 @@
# Ceph Max Storage Rechner # Ceph Max Storage Calculator
Eine moderne Web-Anwendung zur Berechnung der maximal zulässigen Speichernutzung in einem Ceph-Cluster. A modern web application for calculating the maximum allowed storage usage in a Ceph cluster.
## Funktionen ## Features
- Berechnung der maximalen Speichernutzung basierend auf Ceph-Dokumentation - Calculation of maximum storage usage based on Ceph documentation
- Unterstützung für Replikation und Erasure Coding - Support for Replication and Erasure Coding
- Dynamisches Hinzufügen und Entfernen von Nodes und OSDs - Dynamic addition and removal of nodes and OSDs
- Benutzerfreundliche Oberfläche mit modernem Design - User-friendly interface with modern design
- Dark/Light Mode Unterstützung - Dark/Light Mode support
- Export/Import von Konfigurationen - Export/Import of configurations
- PDF-Report Generierung - PDF report generation
- Responsive Design für alle Geräte - Responsive design for all devices
- Ausfalltoleranz-Analyse für Nodes und OSDs - Fault tolerance analysis for nodes and OSDs
## Installation ## Installation
1. Repository klonen: 1. Clone repository:
``` ```
git clone <repository-url> git clone <repository-url>
cd ceph-calculator cd ceph-calculator
``` ```
2. Python-Umgebung einrichten: 2. Set up Python environment:
``` ```
python -m venv venv python -m venv venv
venv\Scripts\activate # Windows venv\Scripts\activate # Windows
source venv/bin/activate # Linux/Mac source venv/bin/activate # Linux/Mac
``` ```
3. Abhängigkeiten installieren: 3. Install dependencies:
``` ```
pip install -r requirements.txt pip install -r requirements.txt
``` ```
4. Anwendung starten: 4. Start application:
``` ```
python run.py python run.py
``` ```
5. Öffnen Sie in einem Browser: `http://localhost:5000` 5. Open in a browser: `http://localhost:5000`
## Verwendung ## Usage
1. Wählen Sie den Replikationstyp: Replikation oder Erasure Coding 1. Select the replication type: Replication or Erasure Coding
2. Geben Sie die entsprechenden Parameter ein: 2. Enter the corresponding parameters:
- Bei Replikation: Anzahl der Replikate und min_size - For Replication: Number of replicas and min_size
- Bei Erasure Coding: k (Datenchunks) und m (Codierungschunks) - For Erasure Coding: k (data chunks) and m (coding chunks)
3. Fügen Sie Nodes hinzu und konfigurieren Sie deren OSDs mit entsprechenden Speichergrößen 3. Add nodes and configure their OSDs with appropriate storage sizes
4. Wählen Sie die gewünschte Speichereinheit (GB/TB) 4. Select the desired storage unit (GB/TB)
5. Klicken Sie auf "Kapazität berechnen", um das Ergebnis zu sehen 5. Click on "Calculate Capacity" to see the result
6. Optional: Exportieren Sie die Konfiguration oder generieren Sie einen PDF-Report 6. Optional: Export the configuration or generate a PDF report
## Technologie ## Technology
- Backend: Flask - Backend: Flask
- Frontend: - Frontend:
- Alpine.js für reaktive Benutzeroberfläche - Alpine.js for reactive user interface
- Tailwind CSS für modernes Design - Tailwind CSS for modern design
- HTMX für interaktive Elemente - HTMX for interactive elements
- Dark/Light Mode mit Tailwind CSS - Dark/Light Mode with Tailwind CSS
- PDF-Generierung mit ReportLab - PDF generation with ReportLab
- Responsive Design für alle Geräte - Responsive design for all devices
## Features im Detail ## Detailed Features
### Replikation ### Replication
- Konfigurierbare Anzahl von Replikaten (1-10) - Configurable number of replicas (1-10)
- Einstellbare min_size für I/O-Operationen - Adjustable min_size for I/O operations
- Automatische Berechnung der Ausfalltoleranz - Automatic calculation of fault tolerance
### Erasure Coding ### Erasure Coding
- Konfigurierbare k/m-Werte - Configurable k/m values
- Optimierte Speichernutzung - Optimized storage usage
- Ausfalltoleranz-Analyse - Fault tolerance analysis
### Ausfalltoleranz-Analyse ### Fault Tolerance Analysis
- Node-Ausfalltoleranz - Node fault tolerance
- OSD-Ausfalltoleranz - OSD fault tolerance
- Multi-Node Ausfalltoleranz - Multi-node fault tolerance
- Detaillierte Informationen zur Speichernutzung nach Ausfällen - Detailed information about storage usage after failures
### Benutzerfreundlichkeit ### User-Friendliness
- Intuitive Benutzeroberfläche - Intuitive user interface
- Dark/Light Mode - Dark/Light Mode
- Tooltips mit Erklärungen - Tooltips with explanations
- Responsive Design - Responsive design
- Export/Import von Konfigurationen - Export/Import of configurations
- PDF-Report Generierung - PDF report generation
## Hinweis ## Note
Die Berechnungen basieren auf den allgemeinen Empfehlungen aus der Ceph-Dokumentation und The calculations are based on general recommendations from the Ceph documentation and
dienen als Richtwert. Für genaue Kapazitätsplanung sollten Sie die Ceph-Dokumentation konsultieren serve as a guideline. For precise capacity planning, you should consult the Ceph documentation
und Ihre spezifischen Clusteranforderungen berücksichtigen. and consider your specific cluster requirements.

View File

@ -17,30 +17,44 @@ def calculate():
try: try:
data = request.json data = request.json
if not data: if not data:
return jsonify({"error": "Keine Daten empfangen"}), 400 return jsonify({"error": "No data received"}), 400
replication_type = data.get('replication_type') # 'replication' oder 'erasure_coding' replication_type = data.get('replication_type') # 'replication' or 'erasure_coding'
if not replication_type: if not replication_type:
return jsonify({"error": "Replikationstyp fehlt"}), 400 return jsonify({"error": "Replication type missing"}), 400
replicas = int(data.get('replicas', 3)) # Standardwert: 3 Replikate # Basic parameter validation
try:
replicas = int(data.get('replicas', 3)) # Default value: 3 replicas
min_size = int(data.get('min_size', 2)) # Default value: 2
# For Erasure Coding
k = int(data.get('k', 0)) if replication_type == 'erasure_coding' else 0
m = int(data.get('m', 0)) if replication_type == 'erasure_coding' else 0
# Nodes and OSDs
nodes = data.get('nodes', [])
if not nodes:
return jsonify({"error": "No nodes defined"}), 400
# Validate node data format
for node in nodes:
if 'osd_count' not in node or 'osd_size_gb' not in node:
return jsonify({"error": "Invalid node data format. Each node must have osd_count and osd_size_gb properties."}), 400
# Simple validity check
if int(node.get('osd_count', 0)) <= 0 or float(node.get('osd_size_gb', 0)) <= 0:
return jsonify({"error": "Invalid OSD data. Both count and size must be greater than 0."}), 400
# Storage unit
storage_unit = data.get('storage_unit', 'GB')
if storage_unit not in ['GB', 'TB']:
storage_unit = 'GB' # Default fallback
except (ValueError, TypeError) as e:
return jsonify({"error": f"Invalid parameter: {str(e)}"}), 400
# Für Erasure Coding # Perform calculation
k = int(data.get('k', 0)) # Datenchunks
m = int(data.get('m', 0)) # Codierungschunks
# Minimale Replikate für I/O-Operationen
min_size = int(data.get('min_size', 2)) # Standardwert: 2
# Nodes und OSDs
nodes = data.get('nodes', [])
if not nodes:
return jsonify({"error": "Keine Nodes definiert"}), 400
# Speichereinheit
storage_unit = data.get('storage_unit', 'GB')
# Berechnung durchführen
result = calculate_ceph_capacity( result = calculate_ceph_capacity(
replication_type=replication_type, replication_type=replication_type,
replicas=replicas, replicas=replicas,
@ -51,39 +65,40 @@ def calculate():
storage_unit=storage_unit storage_unit=storage_unit
) )
logger.info(f"Calculation performed successfully: {replication_type}, replicas={replicas}, nodes={len(nodes)}")
return jsonify(result) return jsonify(result)
except ValueError as e: except ValueError as e:
logger.error(f"Validierungsfehler bei der Berechnung: {str(e)}") logger.error(f"Validation error during calculation: {str(e)}")
return jsonify({"error": str(e)}), 400 return jsonify({"error": str(e)}), 400
except Exception as e: except Exception as e:
logger.error(f"Fehler bei der Berechnung: {str(e)}\n{traceback.format_exc()}") logger.error(f"Error during calculation: {str(e)}\n{traceback.format_exc()}")
return jsonify({"error": "Ein unerwarteter Fehler ist aufgetreten"}), 500 return jsonify({"error": "An unexpected error occurred"}), 500
@bp.route('/generate-pdf', methods=['POST']) @bp.route('/generate-pdf', methods=['POST'])
def generate_pdf(): def generate_pdf():
try: try:
data = request.get_json() data = request.get_json()
if not data: if not data:
return jsonify({"error": "Keine Daten empfangen"}), 400 return jsonify({"error": "No data received"}), 400
# Validiere die Daten # Validate data
if not data.get('replication_type'): if not data.get('replication_type'):
return jsonify({"error": "Replikationstyp fehlt"}), 400 return jsonify({"error": "Replication type missing"}), 400
if not data.get('nodes') or not isinstance(data['nodes'], list): if not data.get('nodes') or not isinstance(data['nodes'], list):
return jsonify({"error": "Ungültige Node-Daten"}), 400 return jsonify({"error": "Invalid node data"}), 400
if not data.get('result') or not isinstance(data['result'], dict): if not data.get('result') or not isinstance(data['result'], dict):
return jsonify({"error": "Ungültige Ergebnisdaten"}), 400 return jsonify({"error": "Invalid result data"}), 400
if not data['result'].get('raw_total'): if not data['result'].get('raw_total'):
return jsonify({"error": "Keine Berechnungsergebnisse vorhanden"}), 400 return jsonify({"error": "No calculation results available"}), 400
# Generiere PDF # Generate PDF
pdf_bytes = generate_pdf_report(data) pdf_bytes = generate_pdf_report(data)
if not pdf_bytes: if not pdf_bytes:
return jsonify({"error": "PDF-Generierung fehlgeschlagen"}), 500 return jsonify({"error": "PDF generation failed"}), 500
return send_file( return send_file(
BytesIO(pdf_bytes), BytesIO(pdf_bytes),
@ -92,8 +107,8 @@ def generate_pdf():
download_name='ceph-report.pdf' download_name='ceph-report.pdf'
) )
except ValueError as e: except ValueError as e:
logger.error(f"Validierungsfehler bei der PDF-Generierung: {str(e)}") logger.error(f"Validation error during PDF generation: {str(e)}")
return jsonify({"error": str(e)}), 400 return jsonify({"error": str(e)}), 400
except Exception as e: except Exception as e:
logger.error(f"Fehler bei der PDF-Generierung: {str(e)}\n{traceback.format_exc()}") logger.error(f"Error during PDF generation: {str(e)}\n{traceback.format_exc()}")
return jsonify({"error": "Ein unerwarteter Fehler ist aufgetreten"}), 500 return jsonify({"error": "An unexpected error occurred"}), 500

BIN
app/static/img/tk-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,167 +0,0 @@
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.');
});
}
};
}

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -9,6 +9,6 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head> </head>
<body> <body>
// ... existing code ...
</body> </body>
</html> </html>

View File

@ -1,15 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de" x-data="{ darkMode: localStorage.getItem('darkMode') === 'true' || (!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches) }" <html lang="en" x-data="{ darkMode: localStorage.getItem('darkMode') === 'true' || (!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches) }"
:class="{ 'dark': darkMode }" :class="{ 'dark': darkMode }"
class="transition-colors duration-500 ease-in-out"> class="transition-colors duration-500 ease-in-out">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}"> <meta name="csrf-token" content="{{ csrf_token() }}">
<title>Ceph Max Storage Rechner</title> <title>Ceph Max Storage Calculator</title>
<!-- HTMX für interaktive Elemente --> <!-- HTMX for interactive elements -->
<script src="https://unpkg.com/htmx.org@1.9.4"></script> <script src="https://unpkg.com/htmx.org@1.9.4"></script>
<!-- Tailwind CSS für modernes Design --> <!-- Tailwind CSS for modern design -->
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
tailwind.config = { tailwind.config = {
@ -46,272 +46,211 @@
} }
} }
</script> </script>
<!-- Alpine.js für reaktive Benutzeroberfläche --> <!-- Alpine.js for reactive user interface -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.0/dist/cdn.min.js"></script> <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> <script src="{{ url_for('static', filename='js/script.js') }}"></script>
</head> </head>
<body class="bg-gray-100 dark:bg-dark-bg min-h-screen transition-all duration-500 ease-in-out" x-data="cephCalculator()"> <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="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"> <div class="max-w-7xl mx-auto">
<!-- Theme Toggle --> <!-- Logo -->
<div class="fixed top-4 right-4 z-50"> <div class="flex justify-center mb-8">
<button @click="darkMode = !darkMode; localStorage.setItem('darkMode', darkMode)" <img src="/static/img/tk-logo.png" alt="Thomas-Krenn.AG Logo" class="h-25 w-auto">
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> </div>
<header class="mb-8"> <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> <h1 class="text-3xl font-bold text-center text-[#333333] dark:text-[#F7941D]">Ceph Max Storage Capacity Calculator</h1>
<p class="text-gray-600 dark:text-gray-400 text-center mt-2">Berechnen Sie die maximale Speichernutzung Ihres Ceph-Clusters</p> <p class="text-gray-600 dark:text-gray-400 text-center mt-2">Calculate the maximum storage usage of your Ceph cluster</p>
</header> </header>
<main class="bg-white dark:bg-dark-card rounded-lg shadow-lg p-6 lg:p-8 transition-colors duration-200"> <main class="bg-white dark:bg-dark-card rounded-lg shadow-lg p-6">
<!-- Replikationstyp --> <!-- Configuration Section -->
<div class="mb-6"> <div class="mb-8">
<h2 class="text-xl font-semibold mb-3 text-gray-800 dark:text-dark-text">Replikationstyp</h2> <h2 class="text-xl font-semibold mb-4 text-[#F7941D] dark:text-[#F7941D]">Configuration</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"> <!-- Replication Type Selection -->
<div class="border p-4 rounded-lg mb-4 bg-gray-50 dark:bg-dark-input dark:border-dark-border"> <div class="mb-6">
<div class="flex flex-wrap items-center gap-4 mb-3"> <label class="block text-[#333333] dark:text-dark-text mb-2">Replication Type</label>
<h3 class="font-medium dark:text-dark-text" x-text="`Node ${nodeIndex + 1}`"></h3> <div class="flex space-x-4">
<button @click="removeNode(nodeIndex)" <button @click="replicationType = 'replication'"
class="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600 ml-auto"> :class="replicationType === 'replication' ? 'bg-[#F7941D] text-white' : 'bg-gray-200 text-[#333333] hover:bg-[#F7941D] hover:text-white'"
Entfernen class="px-4 py-2 rounded-lg transition-colors duration-200">
</button> Replication
</div> </button>
<button @click="replicationType = 'erasure_coding'"
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> :class="replicationType === 'erasure_coding' ? 'bg-[#F7941D] text-white' : 'bg-gray-200 text-[#333333] hover:bg-[#F7941D] hover:text-white'"
<div class="form-group"> class="px-4 py-2 rounded-lg transition-colors duration-200">
<label class="block text-gray-700 dark:text-dark-text mb-2">OSD Anzahl:</label> Erasure Coding
<input type="number" x-model="node.osd_count" min="1" </button>
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>
<!-- Replication Parameters -->
<div x-show="replicationType === 'replication'" class="space-y-4">
<div>
<label class="block text-[#333333] dark:text-dark-text mb-2">Number of Replicas</label>
<input type="number" x-model="replicas" min="1"
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
</div>
<div>
<label class="block text-[#333333] dark:text-dark-text mb-2">Minimum Size</label>
<input type="number" x-model="minSize" min="1"
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
</div>
</div>
<!-- Erasure Coding Parameters -->
<div x-show="replicationType === 'erasure_coding'" class="space-y-4">
<div>
<label class="block text-[#333333] dark:text-dark-text mb-2">Data Chunks (k)</label>
<input type="number" x-model="k" min="1"
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
</div>
<div>
<label class="block text-[#333333] dark:text-dark-text mb-2">Coding Chunks (m)</label>
<input type="number" x-model="m" min="1"
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
</div>
</div>
<!-- Storage Unit Selection -->
<div class="mt-6">
<label class="block text-[#333333] dark:text-dark-text mb-2">Storage Unit</label>
<div class="flex space-x-4">
<button @click="storageUnit = 'TB'"
:class="storageUnit === 'TB' ? 'bg-[#F7941D] text-white' : 'bg-gray-200 text-[#333333] hover:bg-[#F7941D] hover:text-white'"
class="px-4 py-2 rounded-lg transition-colors duration-200">
TB
</button>
<button @click="storageUnit = 'GB'"
:class="storageUnit === 'GB' ? 'bg-[#F7941D] text-white' : 'bg-gray-200 text-[#333333] hover:bg-[#F7941D] hover:text-white'"
class="px-4 py-2 rounded-lg transition-colors duration-200">
GB
</button>
</div>
</div>
</div>
<!-- Nodes Configuration -->
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-[#F7941D] dark:text-[#F7941D]">Nodes Configuration</h2>
<div class="space-y-4">
<template x-for="(node, index) in nodes" :key="index">
<div class="bg-gray-50 dark:bg-dark-input p-4 rounded-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-[#333333] dark:text-dark-text">Node <span x-text="index + 1"></span></h3>
<button @click="removeNode(index)" x-show="nodes.length > 1"
class="text-red-600 hover:text-red-800">
Remove Node
</button>
</div> </div>
<div class="form-group"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="block text-gray-700 dark:text-dark-text mb-2">OSD Größe:</label> <div>
<div class="flex items-center"> <label class="block text-[#333333] dark:text-dark-text mb-2">Number of OSDs</label>
<input type="number" x-model="node.osd_size_gb" min="1" <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"> class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
<span class="ml-2 text-gray-700 dark:text-dark-text" x-text="storageUnit"></span> </div>
<div>
<label class="block text-[#333333] dark:text-dark-text mb-2">OSD Size</label>
<div class="flex">
<input type="number" x-model="node.osd_size_gb" min="1" step="0.1"
class="w-full px-4 py-2 rounded-l-lg border border-gray-300 focus:border-[#F7941D] focus:ring-[#F7941D]">
<span class="inline-flex items-center px-4 py-2 bg-gray-200 text-[#333333] rounded-r-lg border border-l-0 border-gray-300">
<span x-text="storageUnit"></span>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</template> </div>
<button @click="addNode()"
<button @click="addNode()" class="mt-4 px-6 py-2 bg-[#F7941D] text-white rounded-lg hover:bg-[#e88616] transition-colors duration-200">
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"> Add Node
Node hinzufügen
</button> </button>
</div> </div>
<!-- Berechnungsbutton --> <!-- Actions -->
<div class="flex justify-center mt-8 space-x-4"> <div class="flex flex-wrap gap-4 mb-8">
<button @click="calculateCapacity()" <button @click="calculateCapacity()"
class="bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold class="px-6 py-2 bg-[#F7941D] text-white rounded-lg hover:bg-[#e88616] transition-colors duration-200 flex items-center">
shadow-md hover:bg-blue-700 transition duration-300" <span x-show="!isCalculating">Calculate</span>
:disabled="isCalculating"> <span x-show="isCalculating">Calculating...</span>
<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>
<button @click="exportConfig()" <button @click="generatePDF()"
class="bg-green-600 text-white px-6 py-3 rounded-lg font-semibold class="px-6 py-2 border border-[#F7941D] text-[#F7941D] rounded-lg hover:bg-[#F7941D] hover:text-white transition-colors duration-200"
shadow-md hover:bg-green-700 transition duration-300"> x-bind:disabled="!result.raw_total">
Konfiguration exportieren Generate PDF Report
</button> </button>
<label class="bg-purple-600 text-white px-6 py-3 rounded-lg font-semibold <button @click="exportConfig()"
shadow-md hover:bg-purple-700 transition duration-300 cursor-pointer"> class="px-6 py-2 border border-[#F7941D] text-[#F7941D] rounded-lg hover:bg-[#F7941D] hover:text-white transition-colors duration-200">
Konfiguration importieren Export Configuration
</button>
<label class="px-6 py-2 border border-[#F7941D] text-[#F7941D] rounded-lg hover:bg-[#F7941D] hover:text-white transition-colors duration-200 cursor-pointer">
Import Configuration
<input type="file" @change="importConfig($event)" class="hidden" accept=".json"> <input type="file" @change="importConfig($event)" class="hidden" accept=".json">
</label> </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> </div>
<!-- Ergebnisbereich --> <!-- Results -->
<div class="mt-8" x-show="result.raw_total > 0"> <div x-show="result.raw_total" class="space-y-6">
<h2 class="text-xl font-semibold mb-4 text-gray-800 dark:text-dark-text">Ergebnis</h2> <h2 class="text-xl font-semibold text-[#F7941D] dark:text-[#F7941D]">Results</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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="p-3"> <div class="p-3">
<p class="text-gray-600 dark:text-gray-400 mb-1">Gesamt-Rohspeicher:</p> <p class="text-gray-600 dark:text-gray-400 mb-1">Total Raw Storage:</p>
<p class="text-2xl font-bold text-gray-800 dark:text-dark-text"> <p class="text-2xl font-bold text-[#333333] dark:text-dark-text">
<span x-text="storageUnit === 'TB' ? `${(result.raw_total / 1024).toFixed(2)} TB` : `${result.raw_total} GB`"></span> <span x-text="storageUnit === 'TB' ? `${result.raw_total} TB` : `${result.raw_total} GB`"></span>
</p> </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> </div>
<div class="p-3 bg-gray-50 dark:bg-dark-input rounded-lg">
<p class="text-gray-600 dark:text-gray-400 mb-1">Maximum Recommended Usage:</p>
<p class="text-2xl font-bold text-[#F7941D] dark:text-[#F7941D]" x-text="`${result.max_usage_percent}%`"></p>
<p class="text-xl font-semibold text-[#333333] 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 --> <!-- Fault Tolerance -->
<div class="mt-4 border-t border-blue-200 dark:border-dark-border pt-4"> <div class="mt-8">
<h3 class="text-lg font-semibold mb-3 text-gray-800 dark:text-dark-text">Ausfalltoleranz-Analyse</h3> <h2 class="text-xl font-semibold mb-4 text-[#F7941D] dark:text-[#F7941D]">Fault Tolerance</h2>
<div class="space-y-4">
<!-- Node-Ausfalltoleranz --> <div class="p-4 rounded-lg" :class="result.node_failure_tolerance ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'">
<div class="p-3 rounded-lg mb-3" <p class="font-semibold mb-2" :class="result.node_failure_tolerance ? 'text-green-700' : 'text-red-700'">
: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'"> Node Fault Tolerance: <span x-text="result.node_failure_tolerance ? 'Available' : 'Not Available'"></span>
<div class="flex items-center mb-2"> </p>
<span class="font-semibold text-gray-800 dark:text-dark-text mr-2">Node-Ausfalltoleranz:</span> <p class="text-gray-600" x-text="result.node_failure_info"></p>
<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> </div>
<div class="p-4 rounded-lg" :class="result.osd_failure_tolerance ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'">
<!-- OSD-Ausfalltoleranz --> <p class="font-semibold mb-2" :class="result.osd_failure_tolerance ? 'text-green-700' : 'text-red-700'">
<div class="p-3 rounded-lg" OSD Fault Tolerance: <span x-text="result.osd_failure_tolerance ? 'Available' : 'Limited'"></span>
: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'"> </p>
<div class="flex items-center mb-2"> <p class="text-gray-600" x-text="result.osd_failure_info"></p>
<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>
</div> </div>
</div>
</main>
<footer class="mt-8 text-center text-gray-500 dark:text-gray-400 text-sm"> <!-- Recommendations -->
<p>Basierend auf Ceph-Dokumentation | Ergebnisse dienen nur als Richtwert</p> <div class="mt-8">
</footer> <h2 class="text-xl font-semibold mb-4 text-[#F7941D] dark:text-[#F7941D]">Recommendations</h2>
<div class="bg-gray-50 dark:bg-dark-input p-6 rounded-lg border border-gray-200 dark:border-dark-border">
<ul class="list-disc pl-5 space-y-2 text-[#333333] dark:text-dark-text">
<li>Ensure that after node failures, enough capacity remains to avoid reaching the 'full ratio'.</li>
<li>For replication, the number of nodes should be greater than the replication factor.</li>
<li>For erasure coding, the number of nodes should be greater than k+m (data chunks + coding chunks).</li>
<li>Recovery performance depends on network and storage speed.</li>
<li>Note that after a node failure, rebalancing the cluster takes longer the larger the failed node is.</li>
</ul>
</div>
</div>
</div>
<!-- Footer -->
<footer class="mt-12 text-center text-gray-500 dark:text-gray-400 text-sm">
<p>Ceph Storage Calculator &copy; 2025 Thomas-Krenn.AG</p>
</footer>
</main>
</div> </div>
</div> </div>
@ -325,38 +264,31 @@
m: 2, m: 2,
storageUnit: 'TB', storageUnit: 'TB',
nodes: [ nodes: [
{ { osd_count: 4, osd_size_gb: 1 },
osd_count: 4, { osd_count: 4, osd_size_gb: 1 },
osd_size_gb: 1 { osd_count: 4, osd_size_gb: 1 }
}
], ],
result: { result: {
raw_total: 0,
max_usage_percent: 0, max_usage_percent: 0,
max_usage_gb: 0, max_usage_gb: 0,
max_usage_tb: 0, max_usage_tb: 0,
raw_total: 0 node_failure_tolerance: false,
node_failure_info: '',
osd_failure_tolerance: false,
osd_failure_info: '',
multi_failure_tolerance: false,
max_failure_nodes: 0
}, },
isCalculating: false, isCalculating: false,
init() {
const savedConfig = localStorage.getItem('lastConfig');
if (savedConfig) {
const config = JSON.parse(savedConfig);
Object.assign(this, config);
}
},
addNode() { addNode() {
this.nodes.push({ this.nodes.push({ osd_count: 3, osd_size_gb: 2000 });
osd_count: 4,
osd_size_gb: 1
});
}, },
removeNode(nodeIndex) { removeNode(index) {
this.nodes.splice(nodeIndex, 1); if (this.nodes.length > 1) {
if (this.nodes.length === 0) { this.nodes.splice(index, 1);
this.addNode();
} }
}, },
@ -365,120 +297,186 @@
}, },
calculateCapacity() { calculateCapacity() {
this.isCalculating = true;
// Validate Erasure Coding parameters
if (this.replicationType === 'erasure_coding') {
if (parseInt(this.k) <= 0 || parseInt(this.m) <= 0) {
alert('Für Erasure Coding müssen k und m größer als 0 sein.');
this.isCalculating = false;
return;
}
if (parseInt(this.k) + parseInt(this.m) <= 0) {
alert('Die Summe von k und m muss größer als 0 sein.');
this.isCalculating = false;
return;
}
if (this.nodes.length <= parseInt(this.k)) {
alert('Die Anzahl der Nodes muss größer als k sein.');
this.isCalculating = false;
return;
}
}
// Validate OSD sizes
for (let node of this.nodes) {
if (parseFloat(node.osd_size_gb) <= 0) {
alert('Die OSD-Größe muss größer als 0 sein.');
this.isCalculating = false;
return;
}
if (parseInt(node.osd_count) <= 0) {
alert('Die Anzahl der OSDs muss größer als 0 sein.');
this.isCalculating = false;
return;
}
}
// Get CSRF token
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
// Prepare data for sending
const data = { const data = {
replication_type: this.replicationType, replication_type: this.replicationType,
replicas: parseInt(this.replicas), replicas: parseInt(this.replicas),
min_size: parseInt(this.minSize),
k: parseInt(this.k), k: parseInt(this.k),
m: parseInt(this.m), m: parseInt(this.m),
nodes: this.nodes.map(node => ({ nodes: this.nodes.map(node => ({
osd_count: parseInt(node.osd_count), osd_count: parseInt(node.osd_count),
osd_size_gb: this.getOsdSizeInGB(parseFloat(node.osd_size_gb)) osd_size_gb: this.getOsdSizeInGB(parseFloat(node.osd_size_gb))
})), })),
min_size: parseInt(this.minSize),
storage_unit: this.storageUnit storage_unit: this.storageUnit
}; };
this.isCalculating = true; // Send request to server
fetch('/calculate', { fetch('/calculate', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') 'X-CSRFToken': csrfToken
}, },
body: JSON.stringify(data) body: JSON.stringify(data)
}) })
.then(response => response.json()) .then(response => {
.then(data => { if (!response.ok) {
this.result = data; throw new Error('Calculation failed');
}
return response.json();
})
.then(result => {
this.result = result;
this.isCalculating = false;
}) })
.catch(error => { .catch(error => {
console.error('Fehler bei der Berechnung:', error); console.error('Error:', error);
alert('Es ist ein Fehler bei der Berechnung aufgetreten. Bitte versuchen Sie es erneut.');
})
.finally(() => {
this.isCalculating = false; this.isCalculating = false;
alert('Error during calculation: ' + error.message);
}); });
}, },
exportConfig() { 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 = { const data = {
replication_type: this.replicationType, replication_type: this.replicationType,
replicas: this.replicas, replicas: this.replicas,
min_size: this.minSize, min_size: this.minSize,
k: this.k, k: this.k,
m: this.m, m: this.m,
storage_unit: this.storageUnit,
nodes: this.nodes, nodes: this.nodes,
storage_unit: this.storageUnit,
result: this.result result: this.result
}; };
const dataStr = JSON.stringify(data, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = 'ceph-config.json';
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
},
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);
// Update local state with imported configuration
this.replicationType = config.replication_type || 'replication';
this.replicas = config.replicas || 3;
this.minSize = config.min_size || 2;
this.k = config.k || 4;
this.m = config.m || 2;
this.nodes = config.nodes || [{ osd_count: 3, osd_size_gb: 2000 }];
this.storageUnit = config.storage_unit || 'TB';
// If results are included, update those too
if (config.result && config.result.raw_total) {
this.result = config.result;
}
alert('Configuration successfully imported!');
} catch (error) {
console.error('Error importing configuration:', error);
alert('Error importing configuration. Please check the file format.');
}
};
reader.readAsText(file);
},
generatePDF() {
// Get CSRF token
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
// Prepare data for sending
const data = {
replication_type: this.replicationType,
replicas: parseInt(this.replicas),
min_size: parseInt(this.minSize),
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))
})),
storage_unit: this.storageUnit,
result: this.result
};
// Send request to server
fetch('/generate-pdf', { fetch('/generate-pdf', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') 'X-CSRFToken': csrfToken
}, },
body: JSON.stringify(data) body: JSON.stringify(data)
}) })
.then(response => response.blob()) .then(response => {
if (!response.ok) {
throw new Error('PDF generation failed');
}
return response.blob();
})
.then(blob => { .then(blob => {
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.style.display = 'none';
a.href = url; a.href = url;
a.download = 'ceph-report.pdf'; a.download = 'ceph-report.pdf';
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}) })
.catch(error => { .catch(error => {
console.error('Fehler bei der PDF-Generierung:', error); console.error('Error:', error);
alert('Es ist ein Fehler bei der PDF-Generierung aufgetreten. Bitte versuchen Sie es erneut.'); alert('Error generating PDF: ' + error.message);
}); });
} }
}; };

View File

@ -1,19 +1,24 @@
import logging
logger = logging.getLogger(__name__)
def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None, min_size=2, storage_unit='GB'): def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None, min_size=2, storage_unit='GB'):
""" """
Berechnet die maximal zulässige Speichernutzung für ein Ceph-Cluster unter Berücksichtigung von Nodeausfällen. Calculate the maximum allowed storage usage for a Ceph cluster considering node failures.
Parameter:
- replication_type: 'replication' oder 'erasure_coding'
- replicas: Anzahl der Replikate (Standard: 3)
- k: Anzahl der Datenchunks für EC
- m: Anzahl der Codierungschunks für EC
- nodes: Liste der Knoten mit Anzahl der OSDs und deren Größe
[{'osd_count': 4, 'osd_size_gb': 1000}, ...]
- min_size: Minimale Anzahl von Replikaten für I/O-Operationen (Standard: 2)
- storage_unit: 'GB' oder 'TB' (Standard: 'GB')
Args:
replication_type (str): Either 'replication' or 'erasure_coding'
replicas (int): Number of replicas for replication pools (default: 3)
k (int): Number of data chunks for EC
m (int): Number of coding chunks for EC
nodes (list): List of dictionaries with 'osd_count' and 'osd_size_gb' keys
[{'osd_count': 4, 'osd_size_gb': 1000}, ...]
min_size (int): Minimum number of replicas for I/O operations (default: 2)
storage_unit (str): Storage unit, either 'GB' or 'TB' (default: 'GB')
Returns: Returns:
- Dictionary mit max_usage_percent, max_usage_gb, max_usage_tb, raw_total und zusätzlichen Informationen zur Ausfallsicherheit dict: Dictionary with max_usage_percent, max_usage_gb, max_usage_tb, raw_total and additional
information about fault tolerance
""" """
if nodes is None or len(nodes) == 0: if nodes is None or len(nodes) == 0:
return { return {
@ -22,14 +27,11 @@ def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None,
'max_usage_tb': 0, 'max_usage_tb': 0,
'raw_total': 0, 'raw_total': 0,
'node_failure_tolerance': False, 'node_failure_tolerance': False,
'node_failure_info': 'Keine Nodes im Cluster', 'node_failure_info': 'No nodes in the cluster',
'storage_unit': storage_unit 'storage_unit': storage_unit
} }
# Die Umrechnung von TB zu GB ist bereits im JavaScript-Code erfolgt # Calculate total raw storage and information for each node
# Hier werden die Werte direkt verwendet
# Berechne den gesamten Rohspeicher und Informationen zu jedem Node
raw_total_gb = 0 raw_total_gb = 0
node_capacities = [] node_capacities = []
@ -40,80 +42,89 @@ def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None,
node_capacities.append(node_capacity) node_capacities.append(node_capacity)
raw_total_gb += node_capacity raw_total_gb += node_capacity
# Größter Node (worst-case Szenario bei Ausfall) # Largest node (worst-case scenario during failure)
largest_node_capacity = max(node_capacities) if node_capacities else 0 largest_node_capacity = max(node_capacities) if node_capacities else 0
# Berechne die nutzbare Kapazität ohne Ausfall # Calculate usable capacity without failure
if replication_type == 'replication': if replication_type == 'replication':
# Bei Replikation ist der nutzbare Speicher = Rohspeicher / Anzahl der Replikate # For replication, usable storage = raw storage / number of replicas
usable_capacity_gb = raw_total_gb / replicas usable_capacity_gb = raw_total_gb / replicas
else: # Erasure Coding else: # Erasure Coding
# Bei EC ist der nutzbare Speicher = Rohspeicher * (k / (k + m)) # For EC, usable storage = raw storage * (k / (k + m))
if k <= 0 or m <= 0 or (k + m) <= 0:
raise ValueError("Invalid Erasure Coding parameters: k and m must be positive and their sum must be greater than 0")
usable_capacity_gb = raw_total_gb * (k / (k + m)) usable_capacity_gb = raw_total_gb * (k / (k + m))
# Die empfohlene maximale Auslastung für den Normalfall (ohne Ausfall) # Recommended maximum utilization for normal case (without failure)
max_recommended_usage_percent = 80 max_recommended_usage_percent = 80
# Berechne die OSD-Auslastung nach der Formel x = (s × p) / (s + 1) # Calculate OSD utilization using the formula x = (s × p) / (s + 1)
# wobei s = Anzahl der OSDs pro Server und p = Prozentsatz der Gesamtauslastung # where s = number of OSDs per server and p = percentage of total utilization
osds_per_server = nodes[0].get('osd_count', 0) if nodes else 0 osds_per_server = int(nodes[0].get('osd_count', 0)) if nodes else 0
osd_usage_percent = (osds_per_server * max_recommended_usage_percent) / (osds_per_server + 1) osd_usage_percent = (osds_per_server * max_recommended_usage_percent) / (osds_per_server + 1)
# Finde die größte OSD-Größe für die Berechnung der Kapazität nach OSD-Ausfall # Find largest OSD size for calculating capacity after OSD failure
largest_osd_size = max((node.get('osd_size_gb', 0) for node in nodes), default=0) largest_osd_size = max((float(node.get('osd_size_gb', 0)) for node in nodes), default=0)
# Berechne die nutzbare Kapazität nach OSD-Ausfall # Calculate usable capacity after OSD failure
raw_after_osd_failure = raw_total_gb - largest_osd_size raw_after_osd_failure = raw_total_gb - largest_osd_size
if replication_type == 'replication': if replication_type == 'replication':
usable_after_osd_failure = raw_after_osd_failure / replicas usable_after_osd_failure = raw_after_osd_failure / replicas
else: else:
usable_after_osd_failure = raw_after_osd_failure * (k / (k + m)) usable_after_osd_failure = raw_after_osd_failure * (k / (k + m))
# Berechne die maximale sichere Nutzung unter Berücksichtigung von OSD-Ausfall # Calculate maximum safe usage considering OSD failure
max_usage_gb = min( max_usage_gb = min(
usable_capacity_gb * (osd_usage_percent / 100), # OSD-Auslastung basierend auf der Formel usable_capacity_gb * (osd_usage_percent / 100), # OSD utilization based on formula
usable_after_osd_failure * 0.8 # 80% der Kapazität nach OSD-Ausfall usable_after_osd_failure * 0.8 # 80% of capacity after OSD failure
) )
max_usage_tb = max_usage_gb / 1024
# Berechnung der Ausfalltoleranz unter Berücksichtigung von min_size # Convert to TB if storage_unit is TB
if storage_unit == 'TB':
max_usage_tb = max_usage_gb / 1024
else:
max_usage_tb = max_usage_gb / 1024 # Always calculate TB for display
# Calculate fault tolerance considering min_size
if replication_type == 'replication': if replication_type == 'replication':
max_failure_nodes = min( max_failure_nodes = min(
len(nodes) - min_size, # Maximale Ausfälle basierend auf min_size len(nodes) - min_size, # Maximum failures based on min_size
replicas - min_size # Maximale Ausfälle basierend auf Replikationsfaktor replicas - min_size # Maximum failures based on replication factor
) )
else: # Erasure Coding else: # Erasure Coding
max_failure_nodes = min( max_failure_nodes = min(
len(nodes) - (k + 1), # Mindestens k+1 Nodes müssen verfügbar bleiben len(nodes) - (k + 1), # At least k+1 nodes must remain available
m # Maximale Anzahl von Codierungschunks die ausfallen können m # Maximum number of coding chunks that can fail
) )
# Sortiere die Nodes nach Größe absteigend für Worst-Case-Analyse # Sort nodes by size in descending order for worst-case analysis
node_capacities_sorted = sorted(node_capacities, reverse=True) node_capacities_sorted = sorted(node_capacities, reverse=True)
# Kapazität nach Ausfall der größten N Nodes # Capacity after failure of the largest N nodes
raw_after_max_failures_gb = raw_total_gb raw_after_max_failures_gb = raw_total_gb
for i in range(min(max_failure_nodes, len(node_capacities_sorted))): for i in range(min(max_failure_nodes, len(node_capacities_sorted))):
raw_after_max_failures_gb -= node_capacities_sorted[i] raw_after_max_failures_gb -= node_capacities_sorted[i]
# Nutzbare Kapazität nach maximalen tolerierbaren Ausfällen # Usable capacity after maximum tolerable failures
if replication_type == 'replication': if replication_type == 'replication':
usable_after_max_failures_gb = raw_after_max_failures_gb / min_size usable_after_max_failures_gb = raw_after_max_failures_gb / min_size
else: # Erasure Coding else: # Erasure Coding
remaining_m = m - max_failure_nodes remaining_m = m - max_failure_nodes
if remaining_m <= 0:
raise ValueError("Invalid Erasure Coding configuration: remaining coding chunks must be positive")
usable_after_max_failures_gb = raw_after_max_failures_gb * (k / (k + remaining_m)) usable_after_max_failures_gb = raw_after_max_failures_gb * (k / (k + remaining_m))
# Berechne die nutzbare Kapazität nach Ausfall des größten Nodes # Calculate usable capacity after failure of largest node
raw_after_failure_gb = raw_total_gb - largest_node_capacity raw_after_failure_gb = raw_total_gb - largest_node_capacity
if replication_type == 'replication': if replication_type == 'replication':
usable_after_failure_gb = raw_after_failure_gb / min_size usable_after_failure_gb = raw_after_failure_gb / min_size
else: # Erasure Coding else: # Erasure Coding
usable_after_failure_gb = raw_after_failure_gb * (k / (k + m)) usable_after_failure_gb = raw_after_failure_gb * (k / (k + m))
# Prüfen, ob genügend Speicherplatz nach einem Nodeausfall vorhanden ist # Check if there is enough storage space after a node failure
node_failure_tolerance = True node_failure_tolerance = True
# Prüfe die Mindestanforderungen für Nodes # Check minimum requirements for nodes
if replication_type == 'replication': if replication_type == 'replication':
if len(nodes) < min_size: if len(nodes) < min_size:
node_failure_tolerance = False node_failure_tolerance = False
@ -125,7 +136,7 @@ def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None,
elif usable_after_failure_gb < max_usage_gb: elif usable_after_failure_gb < max_usage_gb:
node_failure_tolerance = False node_failure_tolerance = False
# r multiple Ausfälle prüfen # Check for multiple failures
multi_failure_tolerance = False multi_failure_tolerance = False
if max_failure_nodes > 0: if max_failure_nodes > 0:
multi_failure_tolerance = ( multi_failure_tolerance = (
@ -133,7 +144,7 @@ def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None,
len(nodes) > max_failure_nodes len(nodes) > max_failure_nodes
) )
# Maximale sichere Nutzung unter Berücksichtigung eines möglichen Nodeausfalls # Maximum safe usage considering a possible node failure
safe_usage_percent = 0 safe_usage_percent = 0
safe_usage_gb = 0 safe_usage_gb = 0
safe_usage_tb = 0 safe_usage_tb = 0
@ -145,57 +156,57 @@ def calculate_ceph_capacity(replication_type, replicas=3, k=0, m=0, nodes=None,
safe_usage_tb = max_usage_tb safe_usage_tb = max_usage_tb
if multi_failure_tolerance and max_failure_nodes > 1: if multi_failure_tolerance and max_failure_nodes > 1:
node_failure_info = f"Der Cluster kann den Ausfall von bis zu {max_failure_nodes} Nodes tolerieren (min_size={min_size})." node_failure_info = f"The cluster can tolerate failure of up to {max_failure_nodes} nodes (min_size={min_size})."
else: else:
node_failure_info = f"Der Cluster kann einen Ausfall des größten Nodes tolerieren (min_size={min_size})." node_failure_info = f"The cluster can tolerate failure of the largest node (min_size={min_size})."
else: else:
safe_usage_percent = round((usable_after_failure_gb / usable_capacity_gb) * 100 * 0.8) safe_usage_percent = round((usable_after_failure_gb / usable_capacity_gb) * 100 * 0.8)
safe_usage_gb = usable_after_failure_gb * 0.8 safe_usage_gb = usable_after_failure_gb * 0.8
safe_usage_tb = safe_usage_gb / 1024 safe_usage_tb = safe_usage_gb / 1024
if len(nodes) <= (min_size if replication_type == 'replication' else k + m - min(m, 1)): if len(nodes) <= (min_size if replication_type == 'replication' else k + m - min(m, 1)):
node_failure_info = f"KRITISCH: Zu wenige Nodes ({len(nodes)}) für die konfigurierte min_size={min_size}. " node_failure_info = f"CRITICAL: Too few nodes ({len(nodes)}) for the configured min_size={min_size}. "
node_failure_info += f"Mindestens {min_size + 1 if replication_type == 'replication' else k + m + 1 - min(m, 1)} Nodes benötigt." node_failure_info += f"At least {min_size + 1 if replication_type == 'replication' else k + m + 1 - min(m, 1)} nodes needed."
else: else:
# Unit für die Anzeige # Unit for display
unit_display = "TB" if storage_unit == "TB" else "GB" unit_display = "TB" if storage_unit == "TB" else "GB"
node_size_display = round(largest_node_capacity / 1024, 2) if storage_unit == "TB" else round(largest_node_capacity, 2) node_size_display = round(largest_node_capacity / 1024, 2) if storage_unit == "TB" else round(largest_node_capacity, 2)
node_failure_info = (f"WARNUNG: Der Cluster hat nicht genügend freie Kapazität, um einen Ausfall des größten Nodes " node_failure_info = (f"WARNING: The cluster does not have enough free capacity to tolerate a failure of the largest node "
f"({node_size_display} {unit_display}) zu tolerieren. " f"({node_size_display} {unit_display}). "
f"Maximale sichere Nutzung: {safe_usage_percent}%") f"Maximum safe usage: {safe_usage_percent}%")
# Berechnung für den Ausfall eines einzelnen OSD # Calculate for single OSD failure
osd_failure_tolerance = False osd_failure_tolerance = False
osd_failure_info = "Keine OSDs im Cluster" osd_failure_info = "No OSDs in the cluster"
if nodes and any(node.get('osd_count', 0) > 0 for node in nodes): if nodes and any(int(node.get('osd_count', 0)) > 0 for node in nodes):
osd_failure_tolerance = usable_after_osd_failure >= max_usage_gb osd_failure_tolerance = usable_after_osd_failure >= max_usage_gb
# Unit für die Anzeige # Unit for display
unit_display = "TB" if storage_unit == "TB" else "GB" unit_display = "TB" if storage_unit == "TB" else "GB"
osd_size_display = round(largest_osd_size / 1024, 2) if storage_unit == "TB" else round(largest_osd_size, 2) osd_size_display = round(largest_osd_size / 1024, 2) if storage_unit == "TB" else round(largest_osd_size, 2)
osd_failure_info = f"Der Cluster kann den Ausfall des größten OSD tolerieren (min_size={min_size})." if osd_failure_tolerance else \ osd_failure_info = f"The cluster can tolerate failure of the largest OSD (min_size={min_size})." if osd_failure_tolerance else \
f"WARNUNG: Der Cluster hat nicht genügend freie Kapazität, um den Ausfall des größten OSD ({osd_size_display} {unit_display}) zu tolerieren." f"WARNING: The cluster does not have enough free capacity to tolerate failure of the largest OSD ({osd_size_display} {unit_display})."
# Rückgabewerte # Return values with proper unit conversion
result = { result = {
'max_usage_percent': round(osd_usage_percent, 2), 'max_usage_percent': round(osd_usage_percent, 2),
'max_usage_gb': round(max_usage_gb if node_failure_tolerance else safe_usage_gb, 2), 'max_usage_gb': round(max_usage_gb, 2),
'max_usage_tb': round(max_usage_tb if node_failure_tolerance else safe_usage_tb, 2), 'max_usage_tb': round(max_usage_tb, 2),
'raw_total': round(raw_total_gb, 2), 'raw_total': round(raw_total_gb / 1024, 2) if storage_unit == 'TB' else round(raw_total_gb, 2),
'node_failure_tolerance': node_failure_tolerance, 'node_failure_tolerance': node_failure_tolerance,
'node_failure_info': node_failure_info, 'node_failure_info': node_failure_info,
'multi_failure_tolerance': multi_failure_tolerance, 'multi_failure_tolerance': multi_failure_tolerance,
'max_failure_nodes': max_failure_nodes, 'max_failure_nodes': max_failure_nodes,
'osd_failure_tolerance': osd_failure_tolerance, 'osd_failure_tolerance': osd_failure_tolerance,
'osd_failure_info': osd_failure_info, 'osd_failure_info': osd_failure_info,
'largest_node_gb': round(largest_node_capacity, 2), 'largest_node_gb': round(largest_node_capacity / 1024, 2) if storage_unit == 'TB' else round(largest_node_capacity, 2),
'raw_after_failure_gb': round(raw_after_failure_gb, 2), 'raw_after_failure_gb': round(raw_after_failure_gb / 1024, 2) if storage_unit == 'TB' else round(raw_after_failure_gb, 2),
'usable_after_failure_gb': round(usable_after_failure_gb, 2), 'usable_after_failure_gb': round(usable_after_failure_gb / 1024, 2) if storage_unit == 'TB' else round(usable_after_failure_gb, 2),
'raw_after_max_failures_gb': round(raw_after_max_failures_gb, 2), 'raw_after_max_failures_gb': round(raw_after_max_failures_gb / 1024, 2) if storage_unit == 'TB' else round(raw_after_max_failures_gb, 2),
'usable_after_max_failures_gb': round(usable_after_max_failures_gb, 2), 'usable_after_max_failures_gb': round(usable_after_max_failures_gb / 1024, 2) if storage_unit == 'TB' else round(usable_after_max_failures_gb, 2),
'min_size': min_size, 'min_size': min_size,
'osds_per_server': osds_per_server, 'osds_per_server': osds_per_server,
'storage_unit': storage_unit 'storage_unit': storage_unit

View File

@ -1,68 +1,78 @@
from reportlab.lib import colors from reportlab.lib import colors
from reportlab.lib.pagesizes import A4 from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch from reportlab.lib.units import inch
from datetime import datetime from datetime import datetime
import logging import logging
import os
from reportlab.pdfgen import canvas
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Thomas-Krenn Farbschema
TK_ORANGE = colors.HexColor('#F7941D')
TK_DARK_GRAY = colors.HexColor('#333333')
TK_LIGHT_GRAY = colors.HexColor('#F5F5F5')
TK_WHITE = colors.white
TK_GREEN = colors.HexColor('#059669')
TK_RED = colors.HexColor('#dc2626')
def validate_data(data): def validate_data(data):
"""Überprüft, ob alle erforderlichen Daten vorhanden sind.""" """Checks if all required data is present."""
required_fields = ['replication_type', 'nodes', 'result'] required_fields = ['replication_type', 'nodes', 'result']
for field in required_fields: for field in required_fields:
if field not in data: if field not in data:
raise ValueError(f"Fehlendes Feld: {field}") raise ValueError(f"Missing field: {field}")
if not data['nodes']: if not data['nodes']:
raise ValueError("Keine Nodes definiert") raise ValueError("No nodes defined")
if not data['result'].get('raw_total'): if not data['result'].get('raw_total'):
raise ValueError("Keine Berechnungsergebnisse vorhanden") raise ValueError("No calculation results available")
def safe_int(value, default=0): def safe_int(value, default=0):
"""Konvertiert einen Wert sicher in einen Integer.""" """Safely converts a value to an integer."""
if value is None: if value is None:
return default return default
try: try:
return int(str(value).strip()) return int(str(value).strip())
except (ValueError, TypeError): except (ValueError, TypeError):
logger.warning(f"Konnte Wert '{value}' nicht in Integer konvertieren. Verwende Standardwert {default}") logger.warning(f"Could not convert value '{value}' to integer. Using default value {default}")
return default return default
def safe_float(value, default=0.0): def safe_float(value, default=0.0):
"""Konvertiert einen Wert sicher in einen Float.""" """Safely converts a value to a float."""
if value is None: if value is None:
return default return default
try: try:
return float(str(value).strip()) return float(str(value).strip())
except (ValueError, TypeError): except (ValueError, TypeError):
logger.warning(f"Konnte Wert '{value}' nicht in Float konvertieren. Verwende Standardwert {default}") logger.warning(f"Could not convert value '{value}' to float. Using default value {default}")
return default return default
def generate_pdf_report(data): def generate_pdf_report(data):
""" """
Generiert einen PDF-Report mit den Ceph-Konfigurationsdaten und Ergebnissen. Generates a PDF report with Ceph configuration data and results.
Args: Args:
data (dict): Dictionary mit Konfigurations- und Ergebnisdaten data (dict): Dictionary with configuration and result data
Returns: Returns:
bytes: PDF-Datei als Bytes bytes: PDF file as bytes
Raises: Raises:
ValueError: Wenn erforderliche Daten fehlen ValueError: If required data is missing
""" """
try: try:
# Validiere Eingabedaten # Validate input data
validate_data(data) validate_data(data)
# Erstelle einen temporären Buffer für die PDF # Create a temporary buffer for the PDF
from io import BytesIO from io import BytesIO
buffer = BytesIO() buffer = BytesIO()
# Erstelle das PDF-Dokument # Create the PDF document
doc = SimpleDocTemplate( doc = SimpleDocTemplate(
buffer, buffer,
pagesize=A4, pagesize=A4,
@ -70,148 +80,247 @@ def generate_pdf_report(data):
leftMargin=72, leftMargin=72,
topMargin=72, topMargin=72,
bottomMargin=72, bottomMargin=72,
title="Ceph Storage Kapazitätsreport" title="Ceph Storage Capacity Report"
) )
# Styles definieren # Define styles
styles = getSampleStyleSheet() styles = getSampleStyleSheet()
title_style = styles['Heading1']
heading_style = styles['Heading2']
normal_style = styles['Normal']
# Custom Style für Warnungen # Custom styles mit weißer Textfarbe aufgrund des grauen Hintergrunds
title_style = ParagraphStyle(
'CustomTitle',
parent=styles['Heading1'],
fontSize=24,
spaceAfter=30,
textColor=TK_WHITE
)
heading_style = ParagraphStyle(
'CustomHeading',
parent=styles['Heading2'],
fontSize=16,
spaceAfter=12,
textColor=TK_ORANGE
)
normal_style = ParagraphStyle(
'CustomNormal',
parent=styles['Normal'],
fontSize=10,
spaceAfter=6,
textColor=TK_WHITE
)
# Custom Style for warnings
warning_style = ParagraphStyle( warning_style = ParagraphStyle(
'Warning', 'Warning',
parent=styles['Normal'], parent=styles['Normal'],
textColor=colors.red, textColor=TK_RED,
fontSize=10 fontSize=10,
spaceAfter=6
) )
# Story (Inhalt) erstellen # Create story (content)
story = [] story = []
# Titel # Hintergrundfarbe und Canvas-Anpassung
story.append(Paragraph("Ceph Storage Kapazitätsreport", title_style)) # Definiere die Hintergrundfarbe
story.append(Spacer(1, 12)) TK_BACKGROUND_GRAY = colors.HexColor('#1f2937') # Dunkelgrau wie in der Website (dark-bg)
story.append(Paragraph(f"Generiert am: {datetime.now().strftime('%d.%m.%Y %H:%M')}", normal_style))
# Funktion zum Zeichnen des Hintergrunds
def add_page_background(canvas, doc):
canvas.saveState()
canvas.setFillColor(TK_BACKGROUND_GRAY)
canvas.rect(0, 0, doc.pagesize[0], doc.pagesize[1], fill=True, stroke=False)
canvas.restoreState()
# Title with modern styling
story.append(Paragraph("Ceph Storage Capacity Report", title_style))
story.append(Paragraph(f"Generated on: {datetime.now().strftime('%d.%m.%Y %H:%M')}", normal_style))
story.append(Spacer(1, 20)) story.append(Spacer(1, 20))
# Konfigurationsübersicht # Configuration overview with modern styling
story.append(Paragraph("Konfigurationsübersicht", heading_style)) story.append(Paragraph("Configuration Overview", heading_style))
story.append(Spacer(1, 12)) story.append(Spacer(1, 12))
# Replikationstyp # Replication type with modern table style
config_data = [ config_data = [
["Replikationstyp", data['replication_type']], ["Replication Type", data['replication_type']],
["Speichereinheit", data.get('storage_unit', 'GB')] ["Storage Unit", data.get('storage_unit', 'GB')]
] ]
# Add replication-specific parameters
if data['replication_type'] == 'replication': if data['replication_type'] == 'replication':
config_data.extend([ config_data.extend([
["Anzahl Replikate", str(safe_int(data.get('replicas', 3)))], ["Number of Replicas", str(data.get('replicas', 3))],
["Min. Replikate (min_size)", str(safe_int(data.get('min_size', 2)))] ["Minimum Size", str(data.get('min_size', 2))]
]) ])
else: else: # Erasure Coding
config_data.extend([ config_data.extend([
["Datenchunks (k)", str(safe_int(data.get('k', 4)))], ["Data Chunks (k)", str(data.get('k', 4))],
["Codierungschunks (m)", str(safe_int(data.get('m', 2)))] ["Coding Chunks (m)", str(data.get('m', 2))]
]) ])
# Nodes und OSDs # Modern table style for configurations
story.append(Paragraph("Nodes und OSDs", heading_style)) t = Table(config_data, colWidths=[3*inch, 3*inch])
story.append(Spacer(1, 12))
for i, node in enumerate(data['nodes'], 1):
story.append(Paragraph(f"Node {i}", heading_style))
story.append(Spacer(1, 6))
osd_count = safe_int(node.get('osd_count', 1))
osd_size = safe_float(node.get('osd_size_gb', 0))
if osd_count <= 0:
logger.warning(f"Ungültige OSD-Anzahl für Node {i}: {osd_count}. Verwende Standardwert 1.")
osd_count = 1
osd_data = []
for j in range(osd_count):
osd_data.append([f"OSD {j+1}", f"{osd_size} {data.get('storage_unit', 'GB')}"])
t = Table(osd_data, colWidths=[2*inch, 2*inch])
t.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), colors.white),
('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, -1), 12),
('GRID', (0, 0), (-1, -1), 1, colors.black)
]))
story.append(t)
story.append(Spacer(1, 12))
# Ergebnisse
story.append(Paragraph("Ergebnisse", heading_style))
story.append(Spacer(1, 12))
result_data = [
["Gesamt-Rohspeicher", f"{safe_float(data['result']['raw_total'])} GB ({(safe_float(data['result']['raw_total']) / 1024):.2f} TB)"],
["Max. empfohlene Nutzung", f"{safe_float(data['result'].get('max_usage_percent', 0))}%"],
["Max. nutzbarer Speicher", f"{safe_float(data['result'].get('max_usage_gb', 0))} GB ({safe_float(data['result'].get('max_usage_tb', 0))} TB)"]
]
t = Table(result_data, colWidths=[3*inch, 3*inch])
t.setStyle(TableStyle([ t.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), colors.white), ('BACKGROUND', (0, 0), (-1, 0), TK_ORANGE),
('TEXTCOLOR', (0, 0), (-1, -1), colors.black), ('TEXTCOLOR', (0, 0), (-1, 0), TK_WHITE),
('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 10), ('FONTSIZE', (0, 0), (-1, 0), 12),
('BOTTOMPADDING', (0, 0), (-1, -1), 12), ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('GRID', (0, 0), (-1, -1), 1, colors.black) ('BACKGROUND', (0, 1), (-1, -1), TK_DARK_GRAY),
('TEXTCOLOR', (0, 1), (-1, -1), TK_WHITE),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 10),
('ALIGN', (0, 1), (-1, -1), 'LEFT'),
('GRID', (0, 0), (-1, -1), 1, TK_LIGHT_GRAY)
])) ]))
story.append(t) story.append(t)
# Ausfalltoleranz # Add nodes information with modern styling
story.append(Spacer(1, 20)) story.append(Spacer(1, 20))
story.append(Paragraph("Ausfalltoleranz-Analyse", heading_style)) story.append(Paragraph("Nodes Configuration", heading_style))
story.append(Spacer(1, 12)) story.append(Spacer(1, 12))
# Node-Ausfalltoleranz nodes_data = [["Node", "OSD Count", f"OSD Size ({data.get('storage_unit', 'GB')})"]]
node_tolerance = "Vorhanden" if data['result'].get('node_failure_tolerance', False) else "Nicht vorhanden" for i, node in enumerate(data.get('nodes', []), 1):
story.append(Paragraph(f"Node-Ausfalltoleranz: {node_tolerance}", normal_style)) osd_size = safe_float(node.get('osd_size_gb', 0))
story.append(Paragraph(data['result'].get('node_failure_info', 'Keine Informationen verfügbar'), normal_style)) if data.get('storage_unit') == 'TB':
osd_size = osd_size / 1024 # Convert GB to TB for display
nodes_data.append([
f"Node {i}",
str(node.get('osd_count', 0)),
f"{osd_size:.2f}"
])
# Nodes table
t = Table(nodes_data, colWidths=[2*inch, 2*inch, 2*inch])
t.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), TK_ORANGE),
('TEXTCOLOR', (0, 0), (-1, 0), TK_WHITE),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), TK_DARK_GRAY),
('TEXTCOLOR', (0, 1), (-1, -1), TK_WHITE),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 10),
('ALIGN', (0, 1), (-1, -1), 'CENTER'),
('GRID', (0, 0), (-1, -1), 1, TK_LIGHT_GRAY)
]))
story.append(t)
# Results with modern styling
story.append(Spacer(1, 20))
story.append(Paragraph("Results", heading_style))
story.append(Spacer(1, 12))
result_data = [
["Total Raw Storage", f"{safe_float(data['result']['raw_total'])} {data.get('storage_unit', 'GB')}"],
["Max. Recommended Usage", f"{safe_float(data['result'].get('max_usage_percent', 0))}%"],
["Max. Usable Storage", f"{safe_float(data['result'].get('max_usage_tb' if data.get('storage_unit') == 'TB' else 'max_usage_gb', 0))} {data.get('storage_unit', 'GB')}"]
]
# Results table
t = Table(result_data, colWidths=[3*inch, 3*inch])
t.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), TK_ORANGE),
('TEXTCOLOR', (0, 0), (-1, 0), TK_WHITE),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), TK_DARK_GRAY),
('TEXTCOLOR', (0, 1), (-1, -1), TK_WHITE),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 10),
('ALIGN', (0, 1), (-1, -1), 'LEFT'),
('GRID', (0, 0), (-1, -1), 1, TK_LIGHT_GRAY)
]))
story.append(t)
# Fault tolerance with modern styling
story.append(Spacer(1, 20))
story.append(Paragraph("Fault Tolerance Analysis", heading_style))
story.append(Spacer(1, 12))
# Node fault tolerance with status indicator
node_tolerance = "Available" if data['result'].get('node_failure_tolerance', False) else "Not Available"
status_color = TK_GREEN if node_tolerance == "Available" else TK_RED
status_style = ParagraphStyle(
'Status',
parent=normal_style,
textColor=status_color,
fontSize=12,
spaceAfter=6
)
story.append(Paragraph(f"Node Fault Tolerance: {node_tolerance}", status_style))
story.append(Paragraph(data['result'].get('node_failure_info', 'No information available'), normal_style))
if data['result'].get('multi_failure_tolerance', False): if data['result'].get('multi_failure_tolerance', False):
story.append(Paragraph(f"Multi-Node Ausfalltoleranz: {safe_int(data['result'].get('max_failure_nodes', 0))} Nodes", normal_style)) story.append(Paragraph(f"Multi-Node Fault Tolerance: {safe_int(data['result'].get('max_failure_nodes', 0))} Nodes", normal_style))
# OSD-Ausfalltoleranz # OSD fault tolerance with status indicator
story.append(Spacer(1, 12)) story.append(Spacer(1, 12))
osd_tolerance = "Vorhanden" if data['result'].get('osd_failure_tolerance', False) else "Eingeschränkt" osd_tolerance = "Available" if data['result'].get('osd_failure_tolerance', False) else "Limited"
story.append(Paragraph(f"OSD-Ausfalltoleranz: {osd_tolerance}", normal_style)) status_color = TK_GREEN if osd_tolerance == "Available" else TK_RED
story.append(Paragraph(data['result'].get('osd_failure_info', 'Keine Informationen verfügbar'), normal_style)) status_style = ParagraphStyle(
'Status',
parent=normal_style,
textColor=status_color,
fontSize=12,
spaceAfter=6
)
story.append(Paragraph(f"OSD Fault Tolerance: {osd_tolerance}", status_style))
story.append(Paragraph(data['result'].get('osd_failure_info', 'No information available'), normal_style))
# Empfehlungen # Recommendations mit besserem Kontrast für den dunklen Hintergrund
story.append(Spacer(1, 20)) story.append(Spacer(1, 20))
story.append(Paragraph("Empfehlungen", heading_style)) story.append(Paragraph("Recommendations", heading_style))
story.append(Spacer(1, 12)) story.append(Spacer(1, 12))
recommendations = [ recommendations = [
"Stelle sicher, dass nach Nodeausfällen genügend Kapazität vorhanden ist, um die 'full ratio' nicht zu erreichen.", "Ensure that after node failures, enough capacity is available to avoid reaching the 'full ratio'.",
"Bei Replikation sollte die Anzahl der Nodes größer sein als der Replikationsfaktor.", "For replication, the number of nodes should be greater than the replication factor.",
"Bei Erasure Coding sollte die Anzahl der Nodes größer sein als k+m (Datenchunks + Codierungschunks).", "For erasure coding, the number of nodes should be greater than k+m (data chunks + coding chunks).",
"Die Performance bei der Wiederherstellung hängt von der Netzwerk- und Speichergeschwindigkeit ab.", "Recovery performance depends on network and storage speed.",
"Beachte, dass nach einem Nodeausfall das Wiederausbalancieren des Clusters länger dauert, je größer der ausgefallene Node ist." "Note that after a node failure, rebalancing the cluster takes longer the larger the failed node is."
] ]
# Erstelle Box für Empfehlungen mit besserer Lesbarkeit
recommendations_text = []
for rec in recommendations: for rec in recommendations:
story.append(Paragraph(f"{rec}", normal_style)) recommendations_text.append(Paragraph(f"{rec}", normal_style))
# PDF generieren recommendation_table = Table([[item] for item in recommendations_text], colWidths=[6*inch])
doc.build(story) recommendation_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), TK_DARK_GRAY),
('TEXTCOLOR', (0, 0), (-1, -1), TK_WHITE),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('GRID', (0, 0), (-1, -1), 1, TK_LIGHT_GRAY)
]))
story.append(recommendation_table)
# PDF-Bytes zurückgeben # Footer with Thomas-Krenn.AG branding
story.append(Spacer(1, 30))
footer_style = ParagraphStyle(
'Footer',
parent=normal_style,
fontSize=8,
textColor=colors.HexColor('#9ca3af') # Helleres Grau für bessere Lesbarkeit auf dunklem Hintergrund
)
story.append(Paragraph("Generated by Thomas-Krenn.AG Ceph Storage Calculator", footer_style))
# Generate PDF with background
doc.build(story, onFirstPage=add_page_background, onLaterPages=add_page_background)
# Return PDF bytes
return buffer.getvalue() return buffer.getvalue()
except Exception as e: except Exception as e:
logger.error(f"Fehler bei der PDF-Generierung: {str(e)}") logger.error(f"Error during PDF generation: {str(e)}")
raise raise