326 lines
13 KiB
Python
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 |