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:
112
README.md
112
README.md
@ -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.
|
||||||
Binary file not shown.
@ -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
BIN
app/static/img/tk-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@ -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.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
@ -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 © 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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -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
|
||||||
|
|
||||||
# Fü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
|
||||||
|
|||||||
@ -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
|
||||||
Reference in New Issue
Block a user