from fpdf import FPDF import datetime import hashlib class NetBackupMitigationPDF(FPDF): """ Sleek, brand-aligned A4 layout representing the Veritas dark tech design system. """ def header(self): # Draw the top header brand block self.set_fill_color(11, 15, 25) # Deep Midnight Blue (#0B0F19) self.rect(0, 0, 210, 38, 'F') # Brand title text self.set_xy(10, 8) self.set_font('Helvetica', 'B', 16) self.set_text_color(255, 255, 255) self.cell(0, 8, 'NetBackup Log Insights & Actions', ln=True) # Subtitle self.set_font('Helvetica', 'I', 9) self.set_text_color(0, 210, 255) # Cyan Accent self.cell(0, 4, 'Relatorio de Mitigacao e Analise de Performance', ln=True) # Top banner separator line self.set_draw_color(0, 210, 255) self.set_line_width(0.8) self.line(10, 26, 200, 26) self.set_xy(10, 42) # reset cursor below header banner def footer(self): self.set_y(-15) self.set_font('Helvetica', 'I', 8) self.set_text_color(148, 163, 184) # Light Slate self.cell(0, 10, f'Gerado em {datetime.datetime.now().strftime("%d/%m/%Y %H:%M")} | Pagina {self.page_no()}', align='C') def generate_mitigation_pdf(jobs_list, filter_name="Consolidated"): """ Generates a professional PDF summarising backup metrics and logged action steps. """ total_jobs = len(jobs_list) success_jobs = len([j for j in jobs_list if j['exit_code'] <= 1]) success_rate = (success_jobs / total_jobs * 100) if total_jobs > 0 else 0.0 total_pre_dedup = sum(j['mbytes'] for j in jobs_list) # Calculate MSDP simulated post-deduplicated sizes total_post_dedup = 0.0 for j in jobs_list: post_mb = j.get('mbytes', 0.0) * (0.15 + (int(hashlib.md5(str(j['job_id']).encode()).hexdigest(), 16) % 11) / 100.0) total_post_dedup += post_mb dedup_ratio = (total_pre_dedup / total_post_dedup) if total_post_dedup > 0.0 else 1.0 space_saved = ((1 - (total_post_dedup / total_pre_dedup)) * 100) if total_pre_dedup > 0.0 else 0.0 # Count unmitigated failure incidents (Exit Code > 1 AND is_rerun_success == 0 AND status != 'Resolvido') active_failures = len([ j for j in jobs_list if j['exit_code'] > 1 and j['is_rerun_success'] == 0 and j.get('status', 'Pendente') != 'Resolvido' ]) # Create PDF object pdf = NetBackupMitigationPDF() pdf.add_page() pdf.set_auto_page_break(auto=True, margin=20) # Scope indicator pdf.set_font('Helvetica', 'B', 11) pdf.set_text_color(30, 41, 59) pdf.cell(0, 6, f'ZONA DE INFRAESTRUTURA ANALISADA: {filter_name.upper()}', ln=True) pdf.ln(3) # Draw KPI cards row (Success Rate, Dedup, Active Incidents) # Card 1: Success Rate pdf.set_fill_color(30, 38, 64) # Slate Blue pdf.rect(10, 52, 60, 22, 'F') pdf.set_xy(12, 54) pdf.set_font('Helvetica', 'B', 8) pdf.set_text_color(148, 163, 184) pdf.cell(56, 4, 'GLOBAL SUCCESS RATE', ln=True) pdf.set_font('Helvetica', 'B', 14) pdf.set_text_color(0, 200, 83) # Success Green pdf.cell(56, 8, f'{success_rate:.2f}%', ln=True) # Card 2: Dedup Ratio pdf.set_fill_color(30, 38, 64) pdf.rect(75, 52, 60, 22, 'F') pdf.set_xy(77, 54) pdf.set_font('Helvetica', 'B', 8) pdf.set_text_color(148, 163, 184) pdf.cell(56, 4, 'DEDUPLICATION RATIO', ln=True) pdf.set_font('Helvetica', 'B', 14) pdf.set_text_color(0, 210, 255) # Cyan Accent pdf.cell(56, 8, f'{dedup_ratio:.2f}:1', ln=True) # Card 3: Active Failures pdf.set_fill_color(30, 38, 64) pdf.rect(140, 52, 60, 22, 'F') pdf.set_xy(142, 54) pdf.set_font('Helvetica', 'B', 8) pdf.set_text_color(148, 163, 184) pdf.cell(56, 4, 'ACTIVE INCIDENTS', ln=True) pdf.set_font('Helvetica', 'B', 14) pdf.set_text_color(255, 75, 75) # Error Red pdf.cell(56, 8, f'{active_failures}', ln=True) pdf.ln(18) # Storage details section pdf.set_xy(10, 80) pdf.set_font('Helvetica', 'B', 10) pdf.set_text_color(30, 41, 59) pdf.cell(0, 6, 'Volume de Armazenamento MSDP (Deduplicacao):', ln=True) pdf.set_font('Helvetica', '', 9) pdf.cell(0, 5, f'- Volume Pre-Deduplicado: {total_pre_dedup/1024:.2f} GB ({total_pre_dedup:.1f} MB)', ln=True) pdf.cell(0, 5, f'- Volume Post-Deduplicado Gravado: {total_post_dedup/1024:.2f} GB ({total_post_dedup:.1f} MB)', ln=True) pdf.cell(0, 5, f'- Economia de Armazenamento Estimada: {space_saved:.1f}%', ln=True) pdf.ln(6) # Incident Action Log Section pdf.set_font('Helvetica', 'B', 11) pdf.cell(0, 8, 'Acoes de Mitigacao e Status de Falhas de Backup', ln=True) pdf.ln(2) # Filter failures failures_list = [j for j in jobs_list if j['exit_code'] > 1] if not failures_list: pdf.set_font('Helvetica', 'I', 10) pdf.set_text_color(0, 200, 83) pdf.cell(0, 8, 'Nenhuma falha de backup registrada no escopo selecionado.', ln=True) else: # Table Header pdf.set_fill_color(30, 38, 64) pdf.set_font('Helvetica', 'B', 8) pdf.set_text_color(255, 255, 255) pdf.cell(15, 6, 'Job ID', 1, 0, 'C', True) pdf.cell(32, 6, 'Cliente', 1, 0, 'L', True) pdf.cell(35, 6, 'Politica', 1, 0, 'L', True) pdf.cell(15, 6, 'Erro', 1, 0, 'C', True) pdf.cell(28, 6, 'Resolucao Rerun', 1, 0, 'C', True) pdf.cell(20, 6, 'Mitigacao', 1, 0, 'C', True) pdf.cell(45, 6, 'Acao Registrada', 1, 1, 'L', True) # Table Rows formatting pdf.set_font('Helvetica', '', 7.5) pdf.set_text_color(30, 41, 59) fill_row = False for j in failures_list: jid = j['job_id'] client = j['client'] or 'N/A' policy = j['policy'] or 'N/A' code = j['exit_code'] reex = 'Reexecutado' if j['is_rerun_success'] == 1 else 'Pendente' status = j.get('status', 'Pendente') action = j.get('action_taken', '') or 'Nenhuma nota registrada.' # String safety trims if len(client) > 20: client = client[:18] + '..' if len(policy) > 20: policy = policy[:18] + '..' if len(action) > 30: action = action[:28] + '..' # Row alternating colors if fill_row: pdf.set_fill_color(241, 245, 249) else: pdf.set_fill_color(255, 255, 255) pdf.cell(15, 6, str(jid), 1, 0, 'C', True) pdf.cell(32, 6, client, 1, 0, 'L', True) pdf.cell(35, 6, policy, 1, 0, 'L', True) pdf.cell(15, 6, str(code), 1, 0, 'C', True) # Write rerun status with semantic colors if reex == 'Reexecutado': pdf.set_text_color(0, 150, 60) # Green text else: pdf.set_text_color(255, 75, 75) # Red text pdf.cell(28, 6, reex, 1, 0, 'C', True) # Set action status background color pdf.set_text_color(30, 41, 59) if status == 'Resolvido': pdf.set_fill_color(200, 250, 210) # Soft green cell elif status == 'Em Progresso': pdf.set_fill_color(255, 235, 180) # Soft yellow cell else: pdf.set_fill_color(255, 210, 210) # Soft red cell pdf.cell(20, 6, status, 1, 0, 'C', True) # Action notes column if fill_row: pdf.set_fill_color(241, 245, 249) else: pdf.set_fill_color(255, 255, 255) pdf.cell(45, 6, action, 1, 1, 'L', True) fill_row = not fill_row # Output pdf binary data return bytes(pdf.output())