Files
Ceph-Calculator/app/utils/pdf_generator.py

326 lines
13 KiB
Python

from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
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):
"""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"Missing field: {field}")
if not data['nodes']:
raise ValueError("No nodes defined")
if not data['result'].get('raw_total'):
raise ValueError("No calculation results available")
def safe_int(value, default=0):
"""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"Could not convert value '{value}' to integer. Using default value {default}")
return default
def safe_float(value, default=0.0):
"""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"Could not convert value '{value}' to float. Using default value {default}")
return default
def generate_pdf_report(data):
"""
Generates a PDF report with Ceph configuration data and results.
Args:
data (dict): Dictionary with configuration and result data
Returns:
bytes: PDF file as bytes
Raises:
ValueError: If required data is missing
"""
try:
# Validate input data
validate_data(data)
# Create a temporary buffer for the PDF
from io import BytesIO
buffer = BytesIO()
# Create the PDF document
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
rightMargin=72,
leftMargin=72,
topMargin=72,
bottomMargin=72,
title="Ceph Storage Capacity Report"
)
# Define styles
styles = getSampleStyleSheet()
# 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=TK_RED,
fontSize=10,
spaceAfter=6
)
# Create story (content)
story = []
# 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))
# Configuration overview with modern styling
story.append(Paragraph("Configuration Overview", heading_style))
story.append(Spacer(1, 12))
# Replication type with modern table style
config_data = [
["Replication Type", data['replication_type']],
["Storage Unit", data.get('storage_unit', 'GB')]
]
# Add replication-specific parameters
if data['replication_type'] == 'replication':
config_data.extend([
["Number of Replicas", str(data.get('replicas', 3))],
["Minimum Size", str(data.get('min_size', 2))]
])
else: # Erasure Coding
config_data.extend([
["Data Chunks (k)", str(data.get('k', 4))],
["Coding Chunks (m)", str(data.get('m', 2))]
])
# Modern table style for configurations
t = Table(config_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)
# Add nodes information with modern styling
story.append(Spacer(1, 20))
story.append(Paragraph("Nodes Configuration", heading_style))
story.append(Spacer(1, 12))
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 Fault Tolerance: {safe_int(data['result'].get('max_failure_nodes', 0))} Nodes", normal_style))
# OSD fault tolerance with status indicator
story.append(Spacer(1, 12))
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))
# Recommendations mit besserem Kontrast für den dunklen Hintergrund
story.append(Spacer(1, 20))
story.append(Paragraph("Recommendations", heading_style))
story.append(Spacer(1, 12))
recommendations = [
"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:
recommendations_text.append(Paragraph(f"{rec}", normal_style))
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)
# 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"Error during PDF generation: {str(e)}")
raise