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

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

View File

@ -1,68 +1,78 @@
from reportlab.lib import colors
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.units import inch
from datetime import datetime
import logging
import os
from reportlab.pdfgen import canvas
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):
"""Überprüft, ob alle erforderlichen Daten vorhanden sind."""
"""Checks if all required data is present."""
required_fields = ['replication_type', 'nodes', 'result']
for field in required_fields:
if field not in data:
raise ValueError(f"Fehlendes Feld: {field}")
raise ValueError(f"Missing field: {field}")
if not data['nodes']:
raise ValueError("Keine Nodes definiert")
raise ValueError("No nodes defined")
if not data['result'].get('raw_total'):
raise ValueError("Keine Berechnungsergebnisse vorhanden")
raise ValueError("No calculation results available")
def safe_int(value, default=0):
"""Konvertiert einen Wert sicher in einen Integer."""
"""Safely converts a value to an integer."""
if value is None:
return default
try:
return int(str(value).strip())
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
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:
return default
try:
return float(str(value).strip())
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
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:
data (dict): Dictionary mit Konfigurations- und Ergebnisdaten
data (dict): Dictionary with configuration and result data
Returns:
bytes: PDF-Datei als Bytes
bytes: PDF file as bytes
Raises:
ValueError: Wenn erforderliche Daten fehlen
ValueError: If required data is missing
"""
try:
# Validiere Eingabedaten
# Validate input data
validate_data(data)
# Erstelle einen temporären Buffer für die PDF
# Create a temporary buffer for the PDF
from io import BytesIO
buffer = BytesIO()
# Erstelle das PDF-Dokument
# Create the PDF document
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
@ -70,148 +80,247 @@ def generate_pdf_report(data):
leftMargin=72,
topMargin=72,
bottomMargin=72,
title="Ceph Storage Kapazitätsreport"
title="Ceph Storage Capacity Report"
)
# Styles definieren
# Define styles
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',
parent=styles['Normal'],
textColor=colors.red,
fontSize=10
textColor=TK_RED,
fontSize=10,
spaceAfter=6
)
# Story (Inhalt) erstellen
# Create story (content)
story = []
# Titel
story.append(Paragraph("Ceph Storage Kapazitätsreport", title_style))
story.append(Spacer(1, 12))
story.append(Paragraph(f"Generiert am: {datetime.now().strftime('%d.%m.%Y %H:%M')}", normal_style))
# Hintergrundfarbe und Canvas-Anpassung
# Definiere die Hintergrundfarbe
TK_BACKGROUND_GRAY = colors.HexColor('#1f2937') # Dunkelgrau wie in der Website (dark-bg)
# 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))
# Konfigurationsübersicht
story.append(Paragraph("Konfigurationsübersicht", heading_style))
# Configuration overview with modern styling
story.append(Paragraph("Configuration Overview", heading_style))
story.append(Spacer(1, 12))
# Replikationstyp
# Replication type with modern table style
config_data = [
["Replikationstyp", data['replication_type']],
["Speichereinheit", data.get('storage_unit', 'GB')]
["Replication Type", data['replication_type']],
["Storage Unit", data.get('storage_unit', 'GB')]
]
# Add replication-specific parameters
if data['replication_type'] == 'replication':
config_data.extend([
["Anzahl Replikate", str(safe_int(data.get('replicas', 3)))],
["Min. Replikate (min_size)", str(safe_int(data.get('min_size', 2)))]
["Number of Replicas", str(data.get('replicas', 3))],
["Minimum Size", str(data.get('min_size', 2))]
])
else:
else: # Erasure Coding
config_data.extend([
["Datenchunks (k)", str(safe_int(data.get('k', 4)))],
["Codierungschunks (m)", str(safe_int(data.get('m', 2)))]
["Data Chunks (k)", str(data.get('k', 4))],
["Coding Chunks (m)", str(data.get('m', 2))]
])
# Nodes und OSDs
story.append(Paragraph("Nodes und OSDs", heading_style))
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])
# Modern table style for configurations
t = Table(config_data, colWidths=[3*inch, 3*inch])
t.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), colors.white),
('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
('BACKGROUND', (0, 0), (-1, 0), TK_ORANGE),
('TEXTCOLOR', (0, 0), (-1, 0), TK_WHITE),
('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)
('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)
# Ausfalltoleranz
# Add nodes information with modern styling
story.append(Spacer(1, 20))
story.append(Paragraph("Ausfalltoleranz-Analyse", heading_style))
story.append(Paragraph("Nodes Configuration", heading_style))
story.append(Spacer(1, 12))
# Node-Ausfalltoleranz
node_tolerance = "Vorhanden" if data['result'].get('node_failure_tolerance', False) else "Nicht vorhanden"
story.append(Paragraph(f"Node-Ausfalltoleranz: {node_tolerance}", normal_style))
story.append(Paragraph(data['result'].get('node_failure_info', 'Keine Informationen verfügbar'), normal_style))
nodes_data = [["Node", "OSD Count", f"OSD Size ({data.get('storage_unit', 'GB')})"]]
for i, node in enumerate(data.get('nodes', []), 1):
osd_size = safe_float(node.get('osd_size_gb', 0))
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):
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))
osd_tolerance = "Vorhanden" if data['result'].get('osd_failure_tolerance', False) else "Eingeschränkt"
story.append(Paragraph(f"OSD-Ausfalltoleranz: {osd_tolerance}", normal_style))
story.append(Paragraph(data['result'].get('osd_failure_info', 'Keine Informationen verfügbar'), normal_style))
osd_tolerance = "Available" if data['result'].get('osd_failure_tolerance', False) else "Limited"
status_color = TK_GREEN if osd_tolerance == "Available" else TK_RED
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(Paragraph("Empfehlungen", heading_style))
story.append(Paragraph("Recommendations", heading_style))
story.append(Spacer(1, 12))
recommendations = [
"Stelle sicher, dass nach Nodeausfällen genügend Kapazität vorhanden ist, um die 'full ratio' nicht zu erreichen.",
"Bei Replikation sollte die Anzahl der Nodes größer sein als der Replikationsfaktor.",
"Bei Erasure Coding sollte die Anzahl der Nodes größer sein als k+m (Datenchunks + Codierungschunks).",
"Die Performance bei der Wiederherstellung hängt von der Netzwerk- und Speichergeschwindigkeit ab.",
"Beachte, dass nach einem Nodeausfall das Wiederausbalancieren des Clusters länger dauert, je größer der ausgefallene Node ist."
"Ensure that after node failures, enough capacity is available to avoid reaching the 'full ratio'.",
"For replication, the number of nodes should be greater than the replication factor.",
"For erasure coding, the number of nodes should be greater than k+m (data chunks + coding chunks).",
"Recovery performance depends on network and storage speed.",
"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:
story.append(Paragraph(f"{rec}", normal_style))
recommendations_text.append(Paragraph(f"{rec}", normal_style))
# PDF generieren
doc.build(story)
recommendation_table = Table([[item] for item in recommendations_text], colWidths=[6*inch])
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()
except Exception as e:
logger.error(f"Fehler bei der PDF-Generierung: {str(e)}")
logger.error(f"Error during PDF generation: {str(e)}")
raise