from fpdf import FPDF import datetime import hashlib def clean_str(val, default="N/A"): """ Safely casts database records to strings, checking for None and float NaN. """ if val is None: return default # check if float NaN if isinstance(val, float) and val != val: return default s = str(val).strip() return s if s else default 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=0) pdf.set_xy(12, 60) pdf.set_font('Helvetica', 'B', 14) pdf.set_text_color(0, 200, 83) # Success Green pdf.cell(56, 8, f'{success_rate:.2f}%', ln=0) # 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=0) pdf.set_xy(77, 60) 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=0) # 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=0) pdf.set_xy(142, 60) pdf.set_font('Helvetica', 'B', 14) pdf.set_text_color(255, 75, 75) # Error Red pdf.cell(56, 8, f'{active_failures}', ln=0) # Restore position below cards pdf.set_xy(10, 78) # 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(14, 6, 'Job ID', 1, 0, 'C', True) pdf.cell(16, 6, 'Infra/Cloud', 1, 0, 'C', True) pdf.cell(26, 6, 'Cliente', 1, 0, 'L', True) pdf.cell(30, 6, 'Politica', 1, 0, 'L', True) pdf.cell(12, 6, 'Erro', 1, 0, 'C', True) pdf.cell(24, 6, 'Resolucao Rerun', 1, 0, 'C', True) pdf.cell(18, 6, 'Mitigacao', 1, 0, 'C', True) pdf.cell(50, 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 = clean_str(j['client']) policy = clean_str(j['policy']) code = j['exit_code'] reex = 'Reexecutado' if j['is_rerun_success'] == 1 else 'Pendente' status = clean_str(j.get('status'), 'Pendente') action = clean_str(j.get('action_taken'), 'Nenhuma nota registrada.') # String safety trims if len(client) > 16: client = client[:14] + '..' if len(policy) > 18: policy = policy[:16] + '..' if len(action) > 34: action = action[:32] + '..' # Resolve Infra / Cloud based on Primary Server name srv = clean_str(j.get('primary_server'), '').lower() if 'srvpalcvnbu01' in srv: infra = 'Azure' elif 'srvpalcocinbupri01' in srv: infra = 'OCI' else: infra = 'Outro' # Row alternating colors if fill_row: pdf.set_fill_color(241, 245, 249) else: pdf.set_fill_color(255, 255, 255) pdf.cell(14, 6, str(jid), 1, 0, 'C', True) pdf.cell(16, 6, infra, 1, 0, 'C', True) pdf.cell(26, 6, client, 1, 0, 'L', True) pdf.cell(30, 6, policy, 1, 0, 'L', True) pdf.cell(12, 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(24, 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(18, 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(50, 6, action, 1, 1, 'L', True) fill_row = not fill_row # Output pdf binary data return bytes(pdf.output())