Erstelle die Grundstruktur für die Ceph Max Storage Rechner-Anwendung mit Flask und HTMX. Füge Konfigurationsdateien, Routen, Modelle, Templates und statische Dateien hinzu. Implementiere grundlegende Funktionen zur Berechnung der Speichernutzung und zur PDF-Generierung. Integriere CSRF-Schutz und Logging. Stelle sicher, dass die Anwendung modular und wartbar ist.

This commit is contained in:
Samuel Müller
2025-03-26 08:40:32 +01:00
commit f00c1f48e9
26 changed files with 1744 additions and 0 deletions

1
app/utils/__init__.py Normal file
View File

@ -0,0 +1 @@
# Diese Datei macht das Verzeichnis zu einem Python-Paket

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,204 @@
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.
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')
Returns:
- Dictionary mit max_usage_percent, max_usage_gb, max_usage_tb, raw_total und zusätzlichen Informationen zur Ausfallsicherheit
"""
if nodes is None or len(nodes) == 0:
return {
'max_usage_percent': 0,
'max_usage_gb': 0,
'max_usage_tb': 0,
'raw_total': 0,
'node_failure_tolerance': False,
'node_failure_info': 'Keine Nodes im Cluster',
'storage_unit': storage_unit
}
# Die Umrechnung von TB zu GB ist bereits im JavaScript-Code erfolgt
# Hier werden die Werte direkt verwendet
# Berechne den gesamten Rohspeicher und Informationen zu jedem Node
raw_total_gb = 0
node_capacities = []
for node_config in nodes:
osd_count = int(node_config.get('osd_count', 0))
osd_size_gb = float(node_config.get('osd_size_gb', 0))
node_capacity = osd_count * osd_size_gb
node_capacities.append(node_capacity)
raw_total_gb += node_capacity
# Größter Node (worst-case Szenario bei Ausfall)
largest_node_capacity = max(node_capacities) if node_capacities else 0
# Berechne die nutzbare Kapazität ohne Ausfall
if replication_type == 'replication':
# Bei Replikation ist der nutzbare Speicher = Rohspeicher / Anzahl der Replikate
usable_capacity_gb = raw_total_gb / replicas
else: # Erasure Coding
# Bei EC ist der nutzbare Speicher = Rohspeicher * (k / (k + m))
usable_capacity_gb = raw_total_gb * (k / (k + m))
# Die empfohlene maximale Auslastung für den Normalfall (ohne Ausfall)
max_recommended_usage_percent = 80
# Berechne die OSD-Auslastung nach der Formel x = (s × p) / (s + 1)
# wobei s = Anzahl der OSDs pro Server und p = Prozentsatz der Gesamtauslastung
osds_per_server = nodes[0].get('osd_count', 0) if nodes else 0
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
largest_osd_size = max((node.get('osd_size_gb', 0) for node in nodes), default=0)
# Berechne die nutzbare Kapazität nach OSD-Ausfall
raw_after_osd_failure = raw_total_gb - largest_osd_size
if replication_type == 'replication':
usable_after_osd_failure = raw_after_osd_failure / replicas
else:
usable_after_osd_failure = raw_after_osd_failure * (k / (k + m))
# Berechne die maximale sichere Nutzung unter Berücksichtigung von OSD-Ausfall
max_usage_gb = min(
usable_capacity_gb * (osd_usage_percent / 100), # OSD-Auslastung basierend auf der Formel
usable_after_osd_failure * 0.8 # 80% der Kapazität nach OSD-Ausfall
)
max_usage_tb = max_usage_gb / 1024
# Berechnung der Ausfalltoleranz unter Berücksichtigung von min_size
if replication_type == 'replication':
max_failure_nodes = min(
len(nodes) - min_size, # Maximale Ausfälle basierend auf min_size
replicas - min_size # Maximale Ausfälle basierend auf Replikationsfaktor
)
else: # Erasure Coding
max_failure_nodes = min(
len(nodes) - (k + 1), # Mindestens k+1 Nodes müssen verfügbar bleiben
m # Maximale Anzahl von Codierungschunks die ausfallen können
)
# Sortiere die Nodes nach Größe absteigend für Worst-Case-Analyse
node_capacities_sorted = sorted(node_capacities, reverse=True)
# Kapazität nach Ausfall der größten N Nodes
raw_after_max_failures_gb = raw_total_gb
for i in range(min(max_failure_nodes, len(node_capacities_sorted))):
raw_after_max_failures_gb -= node_capacities_sorted[i]
# Nutzbare Kapazität nach maximalen tolerierbaren Ausfällen
if replication_type == 'replication':
usable_after_max_failures_gb = raw_after_max_failures_gb / min_size
else: # Erasure Coding
remaining_m = m - max_failure_nodes
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
raw_after_failure_gb = raw_total_gb - largest_node_capacity
if replication_type == 'replication':
usable_after_failure_gb = raw_after_failure_gb / min_size
else: # Erasure Coding
usable_after_failure_gb = raw_after_failure_gb * (k / (k + m))
# Prüfen, ob genügend Speicherplatz nach einem Nodeausfall vorhanden ist
node_failure_tolerance = True
# Prüfe die Mindestanforderungen für Nodes
if replication_type == 'replication':
if len(nodes) < min_size:
node_failure_tolerance = False
elif usable_after_failure_gb < max_usage_gb:
node_failure_tolerance = False
else: # Erasure Coding
if len(nodes) <= k:
node_failure_tolerance = False
elif usable_after_failure_gb < max_usage_gb:
node_failure_tolerance = False
# Für multiple Ausfälle prüfen
multi_failure_tolerance = False
if max_failure_nodes > 0:
multi_failure_tolerance = (
usable_after_max_failures_gb >= max_usage_gb and
len(nodes) > max_failure_nodes
)
# Maximale sichere Nutzung unter Berücksichtigung eines möglichen Nodeausfalls
safe_usage_percent = 0
safe_usage_gb = 0
safe_usage_tb = 0
node_failure_info = ""
if node_failure_tolerance:
safe_usage_percent = max_recommended_usage_percent
safe_usage_gb = max_usage_gb
safe_usage_tb = max_usage_tb
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})."
else:
node_failure_info = f"Der Cluster kann einen Ausfall des größten Nodes tolerieren (min_size={min_size})."
else:
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_tb = safe_usage_gb / 1024
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"Mindestens {min_size + 1 if replication_type == 'replication' else k + m + 1 - min(m, 1)} Nodes benötigt."
else:
# Unit für die Anzeige
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_failure_info = (f"WARNUNG: Der Cluster hat nicht genügend freie Kapazität, um einen Ausfall des größten Nodes "
f"({node_size_display} {unit_display}) zu tolerieren. "
f"Maximale sichere Nutzung: {safe_usage_percent}%")
# Berechnung für den Ausfall eines einzelnen OSD
osd_failure_tolerance = False
osd_failure_info = "Keine OSDs im Cluster"
if nodes and any(node.get('osd_count', 0) > 0 for node in nodes):
osd_failure_tolerance = usable_after_osd_failure >= max_usage_gb
# Unit für die Anzeige
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_failure_info = f"Der Cluster kann den Ausfall des größten OSD tolerieren (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."
# Rückgabewerte
result = {
'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_tb': round(max_usage_tb if node_failure_tolerance else safe_usage_tb, 2),
'raw_total': round(raw_total_gb, 2),
'node_failure_tolerance': node_failure_tolerance,
'node_failure_info': node_failure_info,
'multi_failure_tolerance': multi_failure_tolerance,
'max_failure_nodes': max_failure_nodes,
'osd_failure_tolerance': osd_failure_tolerance,
'osd_failure_info': osd_failure_info,
'largest_node_gb': round(largest_node_capacity, 2),
'raw_after_failure_gb': round(raw_after_failure_gb, 2),
'usable_after_failure_gb': round(usable_after_failure_gb, 2),
'raw_after_max_failures_gb': round(raw_after_max_failures_gb, 2),
'usable_after_max_failures_gb': round(usable_after_max_failures_gb, 2),
'min_size': min_size,
'osds_per_server': osds_per_server,
'storage_unit': storage_unit
}
return result

217
app/utils/pdf_generator.py Normal file
View File

@ -0,0 +1,217 @@
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
def validate_data(data):
"""Überprüft, ob alle erforderlichen Daten vorhanden sind."""
required_fields = ['replication_type', 'nodes', 'result']
for field in required_fields:
if field not in data:
raise ValueError(f"Fehlendes Feld: {field}")
if not data['nodes']:
raise ValueError("Keine Nodes definiert")
if not data['result'].get('raw_total'):
raise ValueError("Keine Berechnungsergebnisse vorhanden")
def safe_int(value, default=0):
"""Konvertiert einen Wert sicher in einen 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}")
return default
def safe_float(value, default=0.0):
"""Konvertiert einen Wert sicher in einen 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}")
return default
def generate_pdf_report(data):
"""
Generiert einen PDF-Report mit den Ceph-Konfigurationsdaten und Ergebnissen.
Args:
data (dict): Dictionary mit Konfigurations- und Ergebnisdaten
Returns:
bytes: PDF-Datei als Bytes
Raises:
ValueError: Wenn erforderliche Daten fehlen
"""
try:
# Validiere Eingabedaten
validate_data(data)
# Erstelle einen temporären Buffer für die PDF
from io import BytesIO
buffer = BytesIO()
# Erstelle das PDF-Dokument
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
rightMargin=72,
leftMargin=72,
topMargin=72,
bottomMargin=72,
title="Ceph Storage Kapazitätsreport"
)
# Styles definieren
styles = getSampleStyleSheet()
title_style = styles['Heading1']
heading_style = styles['Heading2']
normal_style = styles['Normal']
# Custom Style für Warnungen
warning_style = ParagraphStyle(
'Warning',
parent=styles['Normal'],
textColor=colors.red,
fontSize=10
)
# Story (Inhalt) erstellen
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))
story.append(Spacer(1, 20))
# Konfigurationsübersicht
story.append(Paragraph("Konfigurationsübersicht", heading_style))
story.append(Spacer(1, 12))
# Replikationstyp
config_data = [
["Replikationstyp", data['replication_type']],
["Speichereinheit", data.get('storage_unit', 'GB')]
]
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)))]
])
else:
config_data.extend([
["Datenchunks (k)", str(safe_int(data.get('k', 4)))],
["Codierungschunks (m)", str(safe_int(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])
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)
# Ausfalltoleranz
story.append(Spacer(1, 20))
story.append(Paragraph("Ausfalltoleranz-Analyse", 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))
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))
# OSD-Ausfalltoleranz
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))
# Empfehlungen
story.append(Spacer(1, 20))
story.append(Paragraph("Empfehlungen", 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."
]
for rec in recommendations:
story.append(Paragraph(f"{rec}", normal_style))
# PDF generieren
doc.build(story)
# PDF-Bytes zurückgeben
return buffer.getvalue()
except Exception as e:
logger.error(f"Fehler bei der PDF-Generierung: {str(e)}")
raise