1213 lines
61 KiB
Python
1213 lines
61 KiB
Python
import streamlit as st
|
|
import pandas as pd
|
|
import plotly.express as px
|
|
import plotly.graph_objects as go
|
|
from parser import parse_nbu_csv, compute_hash
|
|
import database as db
|
|
import report_gen as rg
|
|
import time
|
|
import io
|
|
import os
|
|
import json
|
|
|
|
STATUS_CODES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "nbu_status_codes.json")
|
|
PDF_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "NBU_StatusCode.pdf")
|
|
|
|
def compile_status_codes_if_needed():
|
|
"""
|
|
Check if nbu_status_codes.json exists. If not, parse NBU_StatusCode.pdf
|
|
using pypdf to generate it.
|
|
"""
|
|
if os.path.exists(STATUS_CODES_FILE):
|
|
return
|
|
|
|
if not os.path.exists(PDF_FILE):
|
|
return
|
|
|
|
try:
|
|
import pdfplumber
|
|
import re
|
|
|
|
PHRASE_TRANSLATIONS = {
|
|
"cannot connect on socket": "Não foi possível conectar ao socket",
|
|
"client connection refused": "Conexão com o cliente recusada",
|
|
"unable to allocate new media for backup, storage unit has none available": "Não foi possível alocar nova mídia para o backup; a unidade de armazenamento não tem nenhuma disponível",
|
|
"none of the requested files were backed up": "Nenhum dos arquivos solicitados foi salvo (backup falhou)",
|
|
"client/server handshaking failed": "Falha no handshake (aperto de mão) entre cliente e servidor",
|
|
"snapshot error encountered": "Erro de snapshot encontrado",
|
|
"disk is full": "O disco está cheio",
|
|
"file open failed": "Falha ao abrir o arquivo",
|
|
"file read failed": "Falha ao ler o arquivo",
|
|
"file write failed": "Falha ao gravar o arquivo",
|
|
"file close failed": "Falha ao fechar o arquivo",
|
|
"user backup failed": "Falha no backup solicitado pelo usuário",
|
|
"unimplemented feature": "Recurso não implementado",
|
|
"invalid command parameter": "Parâmetro de comando inválido",
|
|
"invalid command usage": "Uso de comando inválido",
|
|
"request timed out": "Tempo limite da requisição esgotado",
|
|
"system call failed": "Falha na chamada de sistema",
|
|
"permission denied": "Permissão negada",
|
|
"write open failed": "Falha ao abrir para gravação",
|
|
"read open failed": "Falha ao abrir para leitura",
|
|
"no storage units available": "Nenhuma unidade de armazenamento disponível",
|
|
"cannot connect to server": "Não foi possível conectar ao servidor",
|
|
"connection timed out": "Tempo limite de conexão esgotado",
|
|
"host is unreachable": "Host/Servidor de destino inacessível",
|
|
"network is unreachable": "Rede inacessível",
|
|
"bad file descriptor": "Descritor de arquivo inválido",
|
|
"no space left on device": "Sem espaço livre no dispositivo",
|
|
"broken pipe": "Pipe quebrado (conexão interrompida)",
|
|
"connection reset by peer": "Conexão redefinida pelo host de destino",
|
|
"address already in use": "Endereço já em uso",
|
|
"host is down": "O host de destino está desligado ou inacessível",
|
|
"too many open files": "Muitos arquivos abertos no sistema",
|
|
"file too large": "Arquivo muito grande",
|
|
"disk quota exceeded": "Cota de disco excedida",
|
|
"stale nfs file handle": "Manipulador de arquivo NFS obsoleto",
|
|
"directory not empty": "O diretório não está vazio",
|
|
"too many users": "Muitos usuários conectados",
|
|
"process limit reached": "Limite de processos atingido",
|
|
"operation canceled": "Operação cancelada",
|
|
"operation now in progress": "Operação em progresso",
|
|
"already in progress": "Já em progresso",
|
|
"bad message": "Mensagem inválida",
|
|
"identifier removed": "Identificador removido",
|
|
"no data available": "Nenhum dado disponível",
|
|
"device not configured": "Dispositivo não configurado",
|
|
"media not present": "Mídia não inserida",
|
|
"write protected": "Mídia protegida contra gravação",
|
|
"hardware error": "Erro de hardware",
|
|
"driver error": "Erro de driver de dispositivo",
|
|
"configuration error": "Erro de configuração",
|
|
"license key expired": "Chave de licença expirada",
|
|
"invalid license key": "Chave de licença inválida",
|
|
"no matching devices found": "Nenhum dispositivo correspondente encontrado",
|
|
"device busy": "Dispositivo ocupado",
|
|
"drive needs cleaning": "A unidade de fita precisa de limpeza",
|
|
"tape alert detected": "Alerta de fita detectado",
|
|
"robotic failure": "Falha na robótica (biblioteca de fitas)",
|
|
"robotic communication failed": "Falha na comunicação com a robótica",
|
|
"robotic device offline": "Dispositivo robótico offline",
|
|
"volume not found": "Volume não encontrado",
|
|
"volume in use": "Volume em uso",
|
|
"volume is read-only": "Volume configurado como somente leitura",
|
|
"cannot mount volume": "Não foi possível montar o volume",
|
|
"cannot unmount volume": "Não foi possível desmontar o volume",
|
|
"drive offline": "Unidade de drive offline",
|
|
"drive in use": "Unidade de drive em uso",
|
|
"drive not ready": "Unidade de drive não pronta",
|
|
"mount failed": "Falha na montagem",
|
|
"unmount failed": "Falha na desmontagem",
|
|
"positioning failed": "Falha no posicionamento da fita",
|
|
"rewind failed": "Falha ao rebobinar",
|
|
"unload failed": "Falha ao descarregar a fita",
|
|
"format failed": "Falha na formatação",
|
|
"database open failed": "Falha ao abrir banco de dados",
|
|
"database read failed": "Falha ao ler banco de dados",
|
|
"database write failed": "Falha ao gravar banco de dados",
|
|
"database close failed": "Falha ao fechar banco de dados",
|
|
"active backup already running": "Backup ativo já em execução para este cliente/política",
|
|
"the requested operation was successfully completed": "A operação solicitada foi concluída com sucesso",
|
|
"the requested operation was partially successful": "A operação solicitada foi parcialmente bem-sucedida",
|
|
"valid archive image produced, but no files deleted due to non-fatal problems": "Imagem de arquivo válida produzida, mas nenhum arquivo foi excluído devido a problemas não fatais",
|
|
"archive file removal failed": "Falha na remoção do arquivo de arquivamento",
|
|
"restore failed completely": "A restauração falhou completamente",
|
|
"the archive failed to backup the requested files": "O arquivamento falhou ao salvar os arquivos solicitados",
|
|
"unable to determine the status of rbak": "Incapaz de determinar o status do rbak",
|
|
"anecessary extension package is not installed or not configured properly": "Um pacote de extensão necessário não está instalado ou configurado corretamente",
|
|
"allocation failed": "Falha de alocação de memória",
|
|
"system call failed": "Falha na chamada de sistema",
|
|
"valid archive image produced but no files deleted due to nonfatal problems": "Imagem de arquivo válida produzida, mas nenhum arquivo foi excluído devido a problemas não fatais",
|
|
"can't connect to client": "Não foi possível conectar ao cliente",
|
|
"none of the requested connections were successful": "Nenhuma das conexões solicitadas foi bem-sucedida"
|
|
}
|
|
|
|
SENTENCE_TRANSLATIONS = {
|
|
"verify that bpcompatd, vnetd, and private branch exchange (pbx) are running.": "Verifique se os serviços bpcompatd, vnetd e Private Branch Exchange (PBX) estão em execução.",
|
|
"verify that the required extension product is installed and configured.": "Verifique se o produto de extensão requerido está instalado e configurado.",
|
|
"free up memory by terminating any unneeded processes that consume memory.": "Libere memória física encerrando quaisquer processos desnecessários em execução.",
|
|
"check the alllogentries and problems reports to determine the system call that failed.": "Verifique os relatórios de 'All Log Entries' (Todos os Logs) e 'Problems' (Problemas) para determinar qual chamada de sistema falhou.",
|
|
"if you want the path for the disk storage unit to reside in the root filesystem:": "Se você deseja que o caminho da unidade de armazenamento de disco resida no sistema de arquivos raiz:",
|
|
"check the netbackup problems report for clues on where and why the problem occurred.": "Consulte o relatório de Problemas do NetBackup para obter pistas sobre onde e por que o problema ocorreu.",
|
|
"check the netbackup problems report to determine the storage unit that is out of media.": "Verifique o relatório de Problemas do NetBackup para determinar qual unidade de armazenamento está sem mídia.",
|
|
"perform the following, as appropriate:": "Execute os seguintes passos conforme apropriado:",
|
|
"do the following, as appropriate:": "Execute os seguintes passos conforme apropriado:",
|
|
"ensure that the appropriate plug-in is configured in the web ui with the appropriate credentials.": "Certifique-se de que o plug-in correto está configurado no painel Web com as credenciais apropriadas.",
|
|
"try to ping the client from the server.": "Tente realizar um teste de ping bidirecional entre o servidor e o cliente.",
|
|
"resolve network communication problems.": "Resolva os problemas de comunicação de rede e liberação de portas.",
|
|
"no action is needed, unless a database was backed up": "Nenhuma ação é necessária, a menos que um banco de dados tenha sido copiado.",
|
|
"a file or a directory path is more than 1023 characters long.": "O caminho de um arquivo ou diretório possui mais de 1023 caracteres.",
|
|
"verify that you have permission to delete the files and that": "Verifique se você possui permissões de exclusão de arquivos aplicadas e que o atributo de somente leitura não está ativo.",
|
|
"verify that you have read access to the files.": "Verifique se o usuário executor possui permissões de acesso de leitura aos arquivos.",
|
|
"check for a new core file to see if rbak quit abnormally.": "Verifique por novos arquivos de dump (core dump) para analisar se o processo rbak terminou de forma anormal.",
|
|
"free up space on the disks where netbackup catalogs reside": "Libere espaço livre nos volumes de disco onde residem os catálogos do NetBackup e tente a operação novamente."
|
|
}
|
|
|
|
WORD_TRANSLATIONS = {
|
|
"cannot": "não é possível", "unable": "não foi possível", "failed": "falhou", "failure": "falha",
|
|
"connection": "conexão", "connect": "conectar", "refused": "recusada", "error": "erro", "client": "cliente",
|
|
"server": "servidor", "socket": "socket", "disk": "disco", "full": "cheio", "file": "arquivo",
|
|
"open": "abrir", "read": "ler", "write": "gravar", "close": "fechar", "media": "mídia",
|
|
"storage": "armazenamento", "unit": "unidade", "available": "disponível", "none": "nenhum",
|
|
"requested": "solicitado", "files": "arquivos", "backed": "salvo", "handshaking": "handshake",
|
|
"snapshot": "snapshot", "encountered": "encontrado", "invalid": "inválido", "parameter": "parâmetro",
|
|
"command": "comando", "usage": "uso", "timeout": "tempo limite esgotado", "database": "banco de dados",
|
|
"volume": "volume", "pool": "pool", "job": "job", "running": "executando", "restore": "restauração",
|
|
"delete": "excluir", "removal": "remoção", "user": "usuário", "permission": "permissão",
|
|
"denied": "negada", "directory": "diretório", "path": "caminho", "unimplemented": "não implementado",
|
|
"feature": "recurso", "empty": "vazio", "exceeded": "excedido", "limit": "limite", "device": "dispositivo",
|
|
"busy": "ocupado", "offline": "offline", "online": "online", "network": "rede", "timed": "esgotado",
|
|
"out": "fora", "verify": "verifique", "check": "verifique", "ensure": "garanta", "please": "por favor",
|
|
"resolving": "resolvendo", "problems": "problemas", "action": "ação", "recommended": "recomendada",
|
|
"the": "o", "and": "e", "or": "ou", "for": "para", "with": "com", "by": "por", "of": "de",
|
|
"is": "está", "are": "estão", "was": "foi", "were": "foram", "that": "que", "not": "não", "no": "não",
|
|
"an": "um", "a": "um", "following": "seguinte", "information": "informação", "applies": "aplica-se",
|
|
"only": "apenas", "using": "usando", "contact": "contate", "your": "seu", "increase": "aumentar",
|
|
"limit": "limite", "see": "veja", "quit": "sair", "abnormally": "anormalmente", "required": "requerido",
|
|
"product": "produto", "installed": "instalado", "configured": "configurado", "free": "liberar",
|
|
"memory": "memória", "terminating": "encerrando", "any": "qualquer", "unneeded": "desnecessário",
|
|
"processes": "processos", "all": "todos", "entries": "entradas", "reports": "relatórios",
|
|
"determine": "determinar", "want": "desejar", "reside": "residir", "root": "raiz", "filesystem": "sistema de arquivos",
|
|
"report": "relatório", "clues": "pistas", "where": "onde", "why": "por que", "problem": "problema",
|
|
"occurred": "ocorreu", "perform": "executar", "as": "como"
|
|
}
|
|
|
|
def translate_sentence(text):
|
|
if not text:
|
|
return ""
|
|
clean_txt = text.strip()
|
|
lower_txt = clean_txt.lower().replace(" ", " ")
|
|
lookup_txt = lower_txt.rstrip(".")
|
|
|
|
if lookup_txt in PHRASE_TRANSLATIONS:
|
|
res = PHRASE_TRANSLATIONS[lookup_txt]
|
|
if lower_txt.endswith("."):
|
|
res += "."
|
|
return res
|
|
if lookup_txt in SENTENCE_TRANSLATIONS:
|
|
res = SENTENCE_TRANSLATIONS[lookup_txt]
|
|
if lower_txt.endswith("."):
|
|
res += "."
|
|
return res
|
|
|
|
bullet_match = re.match(r'^([■•\-\*]\s*)(.*)', clean_txt)
|
|
if bullet_match:
|
|
bullet_symbol = bullet_match.group(1)
|
|
sub_text = bullet_match.group(2)
|
|
return bullet_symbol + translate_sentence(sub_text)
|
|
|
|
words = clean_txt.split()
|
|
translated_words = []
|
|
for w in words:
|
|
w_clean = re.sub(r'[^a-zA-Z\']', '', w).lower()
|
|
if w_clean in WORD_TRANSLATIONS:
|
|
trans = WORD_TRANSLATIONS[w_clean]
|
|
w_trans = w.lower().replace(w_clean, trans)
|
|
if w[0].isupper():
|
|
w_trans = w_trans.capitalize()
|
|
translated_words.append(w_trans)
|
|
else:
|
|
translated_words.append(w)
|
|
return " ".join(translated_words)
|
|
|
|
def translate_text(multiline_text):
|
|
if not multiline_text:
|
|
return ""
|
|
lines = multiline_text.splitlines()
|
|
translated_lines = [translate_sentence(line) for line in lines]
|
|
return "\n".join(translated_lines)
|
|
|
|
def clean_spacing(text):
|
|
if not text:
|
|
return ""
|
|
return re.sub(r'\s+', ' ', text).strip()
|
|
|
|
def get_first_action(action_text):
|
|
if not action_text:
|
|
return "Nenhuma ação recomendada específica encontrada no manual."
|
|
lines = [line.strip() for line in action_text.splitlines() if line.strip()]
|
|
if not lines:
|
|
return "Nenhuma ação recomendada específica encontrada no manual."
|
|
bullets = ['■', '-', '*', '•']
|
|
first_bullet = None
|
|
for line in lines:
|
|
if any(line.startswith(b) for b in bullets) or re.match(r'^\d+\.', line):
|
|
cleaned_line = line
|
|
for b in bullets:
|
|
if cleaned_line.startswith(b):
|
|
cleaned_line = cleaned_line[len(b):].strip()
|
|
break
|
|
first_bullet = cleaned_line
|
|
break
|
|
if first_bullet:
|
|
return first_bullet
|
|
for line in lines:
|
|
if line.endswith(':') and len(line) < 40:
|
|
continue
|
|
return line
|
|
return lines[0]
|
|
|
|
parsed = {}
|
|
with pdfplumber.open(PDF_FILE) as pdf:
|
|
full_text_list = []
|
|
for page in pdf.pages:
|
|
text = page.extract_text(x_tolerance=2.0)
|
|
if text:
|
|
full_text_list.append(text)
|
|
full_text = "\n".join(full_text_list)
|
|
|
|
pattern = re.compile(r'NetBackup\s*status\s*code\s*:\s*(\d+)', re.IGNORECASE)
|
|
matches = list(pattern.finditer(full_text))
|
|
|
|
for idx, match in enumerate(matches):
|
|
code_str = match.group(1)
|
|
code_num = int(code_str)
|
|
start_pos = match.start()
|
|
end_pos = matches[idx + 1].start() if idx + 1 < len(matches) else len(full_text)
|
|
|
|
chunk = full_text[start_pos:end_pos]
|
|
msg_match = re.search(r'Message\s*:\s*(.*)', chunk, re.IGNORECASE)
|
|
if msg_match:
|
|
expl_match = re.search(r'Explanation\s*:\s*', chunk, re.IGNORECASE)
|
|
desc = ""
|
|
msg_header_match = re.search(r'Message\s*:\s*', chunk, re.IGNORECASE)
|
|
msg_start = msg_header_match.end()
|
|
|
|
if expl_match:
|
|
desc = chunk[msg_start:expl_match.start()].strip()
|
|
else:
|
|
desc = chunk[msg_start:].strip()
|
|
desc = clean_spacing(desc)
|
|
|
|
rec_match = re.search(r'Recommended\s*Action\s*:\s*', chunk, re.IGNORECASE)
|
|
action_full = ""
|
|
if rec_match:
|
|
action_start = rec_match.end()
|
|
click_match = re.search(r'Click\s*here\s*to\s*view\s*technical\s*notes', chunk[action_start:], re.IGNORECASE)
|
|
if click_match:
|
|
action_end = action_start + click_match.start()
|
|
else:
|
|
action_end = len(chunk)
|
|
action_full = chunk[action_start:action_end].strip()
|
|
else:
|
|
action_full = "No specific recommended action found in the manual."
|
|
|
|
action_full = re.sub(r'\d+\s*NetBackup\s*status\s*codes', '', action_full, flags=re.IGNORECASE)
|
|
action_full = re.sub(r'NetBackup\s*status\s*codes', '', action_full, flags=re.IGNORECASE)
|
|
|
|
lines = [line.strip() for line in action_full.splitlines() if line.strip()]
|
|
action_clean = "\n".join(lines)
|
|
|
|
desc_pt = translate_sentence(desc)
|
|
action_clean_pt = translate_text(action_clean)
|
|
first_action_eng = get_first_action(action_clean)
|
|
first_action_pt = translate_sentence(first_action_eng)
|
|
|
|
if desc_pt and desc_pt[0].islower():
|
|
desc_pt = desc_pt[0].upper() + desc_pt[1:]
|
|
if first_action_pt and first_action_pt[0].islower():
|
|
first_action_pt = first_action_pt[0].upper() + first_action_pt[1:]
|
|
|
|
parsed[code_num] = {
|
|
"code": code_num,
|
|
"desc": desc_pt,
|
|
"first_action": first_action_pt,
|
|
"full_action": action_clean_pt
|
|
}
|
|
|
|
with open(STATUS_CODES_FILE, "w", encoding="utf-8") as f:
|
|
json.dump({str(k): v for k, v in parsed.items()}, f, indent=4, ensure_ascii=False)
|
|
except Exception as e:
|
|
print(f"Error compiling status codes: {e}")
|
|
|
|
|
|
# Compile on load if needed
|
|
compile_status_codes_if_needed()
|
|
|
|
# Load compiled database
|
|
NBU_STATUS_CODES = {}
|
|
if os.path.exists(STATUS_CODES_FILE):
|
|
try:
|
|
with open(STATUS_CODES_FILE, "r", encoding="utf-8") as f:
|
|
NBU_STATUS_CODES = json.load(f)
|
|
except Exception as e:
|
|
print(f"Error loading status codes: {e}")
|
|
|
|
def get_status_code_info(code):
|
|
"""
|
|
Resolves troubleshooting steps from the offline database compiled from the PDF.
|
|
If the code is one of the common codes, it combines local Portuguese knowledge with the PDF.
|
|
"""
|
|
local_dict = {
|
|
2: {
|
|
"desc": "Conexões de rede não sucedidas (None of the requested connections were successful)",
|
|
"action": "Ação Prioritária: Falha de comunicação entre o Servidor de Backup e o Cliente.\n1. Teste ping bidirecional entre o Master/Media e o cliente.\n2. Verifique a resolução de nomes (DNS / arquivos hosts).\n3. Verifique se as portas 1556 (PBX) e 13724 (vnetd) estão liberadas na rede."
|
|
},
|
|
25: {
|
|
"desc": "Impossível conectar ao socket do daemon (Cannot connect on socket)",
|
|
"action": "Ação Prioritária: O serviço do NetBackup Client não está respondendo.\n1. Verifique se o serviço 'NetBackup Client Service' (bpcd) está iniciado no cliente.\n2. Execute 'bptestbpcd -client <cliente>' do Master Server para diagnosticar."
|
|
},
|
|
26: {
|
|
"desc": "Erro de gravação no socket pelo cliente (Client crashed or connection dropped)",
|
|
"action": "Ação Prioritária: O cliente interrompeu a transmissão abruptamente.\n1. Monitore a estabilidade física da rede durante o backup.\n2. Verifique logs de eventos do sistema operacional no cliente por falta de memória (OOM) ou pânico do kernel."
|
|
},
|
|
57: {
|
|
"desc": "Conexão com o Media Manager falhou (Media manager connection failed)",
|
|
"action": "Ação Prioritária: Problema de comunicação com o Media Server.\n1. Certifique-se de que os daemons de controle de mídia e robótica (ltid, etc.) estão rodando no Media Server.\n2. Verifique se os dispositivos de fita ou storage pools estão online."
|
|
},
|
|
58: {
|
|
"desc": "Estouro de tempo limite na comunicação com o cliente (Can't connect to client / Timeout)",
|
|
"action": "Ação Prioritária: Conexão bloqueada por Firewall ou serviço inativo.\n1. Libere a porta TCP 1556 nos firewalls intermediários e locais do cliente.\n2. Confirme se o IP do Master/Media Server está listado nas configurações de servidores autorizados do cliente."
|
|
},
|
|
96: {
|
|
"desc": "Sem mídias ou volumes disponíveis no pool (Unable to allocate new media)",
|
|
"action": "Ação Prioritária: Esgotamento de espaço físico ou lógico de armazenamento.\n1. Adicione mídias virgens ou volumes extras ao volume pool da Storage Unit.\n2. Verifique no painel de mídias se há fitas presas no estado 'frozen' ou 'suspended' e execute o comando para liberá-las: bpmedia -unfreeze -m <media_id>."
|
|
},
|
|
156: {
|
|
"desc": "Falha na criação do Snapshot da máquina virtual (Snapshot creation failed)",
|
|
"action": "Ação Prioritária: Falha na API de snapshot da infraestrutura de virtualização (vCenter/Hyper-V) ou VSS.\n1. Verifique se a VM possui snapshots antigos presos e execute a consolidação.\n2. Confirme se há espaço livre disponível no Datastore de destino da VM.\n3. Reinicie o serviço de Shadow Copy (VSS) caso seja cliente Windows."
|
|
}
|
|
}
|
|
|
|
try:
|
|
code_int = int(code)
|
|
except Exception:
|
|
code_int = 0
|
|
|
|
code_str = str(code_int)
|
|
|
|
pdf_desc = ""
|
|
pdf_first_action = ""
|
|
pdf_full_action = ""
|
|
|
|
if code_str in NBU_STATUS_CODES:
|
|
info = NBU_STATUS_CODES[code_str]
|
|
pdf_desc = info.get("desc", "")
|
|
pdf_first_action = info.get("first_action", "")
|
|
pdf_full_action = info.get("full_action", "")
|
|
|
|
desc = ""
|
|
action = ""
|
|
|
|
if code_int in local_dict:
|
|
if pdf_desc:
|
|
desc = f"{local_dict[code_int]['desc']} (PDF: {pdf_desc})"
|
|
else:
|
|
desc = local_dict[code_int]['desc']
|
|
|
|
local_act = local_dict[code_int]['action']
|
|
if pdf_first_action:
|
|
action = f"{local_act}\n\n💡 [Manual PDF - Primeira Ação Recomendada]:\n{pdf_first_action}"
|
|
else:
|
|
action = local_act
|
|
else:
|
|
if pdf_desc:
|
|
desc = f"PDF Description: {pdf_desc}"
|
|
else:
|
|
desc = f"Código de status {code_int} do NetBackup"
|
|
|
|
if pdf_first_action:
|
|
action = f"Ação Recomendada (Manual PDF - Primeira Ação):\n{pdf_first_action}"
|
|
if pdf_full_action:
|
|
action += f"\n\nOutras ações descritas no manual:\n{pdf_full_action}"
|
|
else:
|
|
action = f"Ação Recomendada:\nInvestigue os logs do NetBackup (Activity Monitor) para este código de erro."
|
|
|
|
return {
|
|
"desc": desc,
|
|
"action": action
|
|
}
|
|
|
|
# Initialize database schemas
|
|
db.init_db()
|
|
|
|
# Set up page configurations
|
|
st.set_page_config(
|
|
page_title="NetBackup Log Insights",
|
|
page_icon="⚡",
|
|
layout="wide",
|
|
initial_sidebar_state="expanded"
|
|
)
|
|
|
|
# Custom CSS for high-fidelity dark corporate theme (CXP-inspired)
|
|
custom_css = """
|
|
<style>
|
|
/* Main App Background & Text */
|
|
.stApp {
|
|
background-color: #0B0F19;
|
|
color: #F1F5F9; /* High contrast off-white */
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
}
|
|
|
|
/* Headers styling */
|
|
h1, h2, h3, h4, h5, h6 {
|
|
color: #FFFFFF !important;
|
|
font-weight: 700 !important;
|
|
}
|
|
|
|
/* Sidebar Styling */
|
|
section[data-testid="stSidebar"] {
|
|
background-color: #070A13 !important; /* Darker sidebar background for contrast */
|
|
border-right: 1px solid #1E293B;
|
|
}
|
|
section[data-testid="stSidebar"] h1,
|
|
section[data-testid="stSidebar"] h2,
|
|
section[data-testid="stSidebar"] h3 {
|
|
color: #00D2FF !important;
|
|
}
|
|
|
|
/* Metric Card Styling */
|
|
div[data-testid="stMetric"] {
|
|
background-color: #161E35 !important; /* Slate blue card for high contrast */
|
|
border: 1px solid #3B82F6; /* Blue border */
|
|
border-radius: 12px;
|
|
padding: 20px !important;
|
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
|
|
transition: all 0.3s ease;
|
|
}
|
|
div[data-testid="stMetric"]:hover {
|
|
border-color: #00D2FF;
|
|
transform: translateY(-2px);
|
|
}
|
|
div[data-testid="stMetric"] label {
|
|
color: #CBD5E1 !important; /* High contrast silver-gray */
|
|
font-size: 0.85rem !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
font-weight: 600;
|
|
}
|
|
div[data-testid="stMetric"] div[data-testid="stMetricValue"] {
|
|
color: #FFFFFF !important; /* Crisp white value */
|
|
font-size: 2.2rem !important;
|
|
font-weight: 800;
|
|
}
|
|
|
|
/* Form inputs and buttons styling */
|
|
.stSelectbox, .stTextInput, .stTextArea, .stFileUploader {
|
|
background-color: #161E35 !important;
|
|
color: #FFFFFF !important;
|
|
border-radius: 8px;
|
|
border: 1px solid #2E3A5F;
|
|
}
|
|
|
|
/* Buttons styling */
|
|
div.stButton > button, div.stDownloadButton > button {
|
|
background: linear-gradient(135deg, #0052CC 0%, #00D2FF 100%) !important;
|
|
color: #FFFFFF !important;
|
|
border: none !important;
|
|
border-radius: 8px !important;
|
|
padding: 0.6rem 1.8rem !important;
|
|
font-weight: 700 !important;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
|
box-shadow: 0 4px 14px rgba(0, 210, 255, 0.25) !important;
|
|
text-transform: uppercase;
|
|
font-size: 0.85rem;
|
|
letter-spacing: 0.05em;
|
|
width: 100%;
|
|
}
|
|
div.stButton > button:hover, div.stDownloadButton > button:hover {
|
|
background: linear-gradient(135deg, #00D2FF 0%, #0052CC 100%) !important;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 22px rgba(0, 210, 255, 0.5) !important;
|
|
}
|
|
|
|
/* Badges */
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 0.25em 0.6em;
|
|
font-size: 80%;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
vertical-align: baseline;
|
|
border-radius: 6px;
|
|
}
|
|
.badge-success {
|
|
background-color: rgba(0, 200, 83, 0.25);
|
|
color: #00E676; /* Brighter green */
|
|
border: 1px solid #00E676;
|
|
}
|
|
.badge-warning {
|
|
background-color: rgba(255, 171, 0, 0.25);
|
|
color: #FFD600; /* Brighter yellow */
|
|
border: 1px solid #FFD600;
|
|
}
|
|
|
|
/* Tabs selector customization */
|
|
button[data-baseweb="tab"] {
|
|
color: #94A3B8 !important;
|
|
font-size: 1.1rem !important;
|
|
font-weight: 600 !important;
|
|
padding: 10px 20px !important;
|
|
border-bottom: 3px solid transparent !important;
|
|
transition: all 0.2s ease !important;
|
|
}
|
|
button[data-baseweb="tab"][aria-selected="true"] {
|
|
color: #00D2FF !important;
|
|
border-bottom: 3px solid #00D2FF !important;
|
|
background-color: rgba(30, 38, 64, 0.3) !important;
|
|
}
|
|
|
|
/* Table headers customize */
|
|
div[data-testid="stDataFrame"] {
|
|
background-color: #161E35 !important;
|
|
border: 1px solid #2E3A5F;
|
|
border-radius: 12px;
|
|
padding: 8px;
|
|
}
|
|
</style>
|
|
"""
|
|
st.markdown(custom_css, unsafe_allow_html=True)
|
|
|
|
# Initialize Authentication Session States
|
|
if 'logged_in' not in st.session_state:
|
|
st.session_state['logged_in'] = False
|
|
if 'username' not in st.session_state:
|
|
st.session_state['username'] = ""
|
|
if 'role' not in st.session_state:
|
|
st.session_state['role'] = ""
|
|
|
|
# --- ROUTING ENGINE ---
|
|
|
|
# Scenario 1: First Run Bootstrap (No Administrator exists)
|
|
if not db.has_admin_user():
|
|
st.markdown("<div style='text-align: center; margin-top: 50px;'>", unsafe_allow_html=True)
|
|
st.markdown("<h1>⚙️ Configuração Inicial do Sistema</h1>", unsafe_allow_html=True)
|
|
st.markdown("<p style='color: #94A3B8; font-size: 1.1rem;'>Nenhum usuário administrador encontrado no banco de dados. Crie o Administrador Principal para continuar.</p>", unsafe_allow_html=True)
|
|
st.markdown("</div>", unsafe_allow_html=True)
|
|
|
|
col_boot1, col_boot2, col_boot3 = st.columns([1, 1.3, 1])
|
|
with col_boot2:
|
|
with st.form("bootstrap_form"):
|
|
admin_user = st.text_input("Usuário Administrador:", placeholder="Ex: admin")
|
|
admin_pass = st.text_input("Senha do Administrador:", type="password", placeholder="Digite uma senha robusta")
|
|
admin_pass_confirm = st.text_input("Confirme a Senha:", type="password", placeholder="Confirme a senha")
|
|
submit_bootstrap = st.form_submit_button("Criar Administrador Principal")
|
|
|
|
if submit_bootstrap:
|
|
if not admin_user.strip():
|
|
st.error("O nome de usuário não pode estar em branco.")
|
|
elif len(admin_pass) < 6:
|
|
st.error("A senha deve conter pelo menos 6 caracteres.")
|
|
elif admin_pass != admin_pass_confirm:
|
|
st.error("As senhas informadas não coincidem.")
|
|
else:
|
|
success = db.create_user(admin_user, admin_pass, role='admin')
|
|
if success:
|
|
st.success("Administrador Principal cadastrado com sucesso! Recarregando...")
|
|
time.sleep(1.5)
|
|
st.rerun()
|
|
else:
|
|
st.error("Erro ao cadastrar. O usuário já existe ou ocorreu um erro de persistência.")
|
|
st.stop()
|
|
|
|
# Scenario 2: Unauthenticated User (Login Gate)
|
|
if not st.session_state['logged_in']:
|
|
col_login1, col_login2, col_login3 = st.columns([1, 1.2, 1])
|
|
with col_login2:
|
|
st.markdown("<div style='text-align: center; margin-top: 80px;'>", unsafe_allow_html=True)
|
|
st.markdown("<h1>⚡ NetBackup Log Insights</h1>", unsafe_allow_html=True)
|
|
st.markdown("<p style='color: #94A3B8;'>Faça login para gerenciar as rotinas de backup e mitigações</p>", unsafe_allow_html=True)
|
|
st.markdown("</div>", unsafe_allow_html=True)
|
|
|
|
with st.form("login_form"):
|
|
login_user = st.text_input("Usuário:", placeholder="Nome do usuário")
|
|
login_pass = st.text_input("Senha:", type="password", placeholder="Senha de acesso")
|
|
submit_login = st.form_submit_button("Entrar")
|
|
|
|
if submit_login:
|
|
user_record = db.get_user(login_user)
|
|
if user_record and db.verify_password(user_record['password_hash'], login_pass):
|
|
st.session_state['logged_in'] = True
|
|
st.session_state['username'] = user_record['username']
|
|
st.session_state['role'] = user_record['role']
|
|
st.toast(f"Bem-vindo, {user_record['username']}!", icon="👋")
|
|
time.sleep(0.5)
|
|
st.rerun()
|
|
else:
|
|
st.error("Usuário ou senha inválidos.")
|
|
st.stop()
|
|
|
|
# --- APP LAYOUT (Authenticated Scope) ---
|
|
|
|
# Sidebar Profile Header
|
|
st.sidebar.markdown(f"""
|
|
<div style='background-color: #1E2640; padding: 15px; border-radius: 12px; border: 1px solid #2E3A5F; margin-bottom: 25px; text-align: center;'>
|
|
<p style='margin: 0; font-size: 0.8rem; color: #94A3B8; text-transform: uppercase; letter-spacing: 0.05em;'>Técnico Autenticado</p>
|
|
<h3 style='margin: 5px 0; color: #FFFFFF;'>{st.session_state['username'].upper()}</h3>
|
|
<span class='badge {"badge-success" if st.session_state["role"] == "admin" else "badge-warning"}'>
|
|
{st.session_state["role"].upper()}
|
|
</span>
|
|
</div>
|
|
""", unsafe_allow_html=True)
|
|
|
|
# Logout Button
|
|
if st.sidebar.button("Encerrar Sessão (Sair)"):
|
|
st.session_state['logged_in'] = False
|
|
st.session_state['username'] = ""
|
|
st.session_state['role'] = ""
|
|
st.rerun()
|
|
|
|
st.sidebar.markdown("---")
|
|
|
|
# Global zone selector
|
|
st.sidebar.subheader("Zone Selector")
|
|
server_filter = st.sidebar.selectbox(
|
|
"Selecione o escopo:",
|
|
options=[
|
|
"Consolidated View (Geral)",
|
|
"Azure Infrastructure Zone",
|
|
"OCI Infrastructure Zone"
|
|
]
|
|
)
|
|
|
|
# File Ingestion Panel
|
|
st.sidebar.subheader("Log Ingestion")
|
|
uploaded_file = st.sidebar.file_uploader(
|
|
"Importar relatório NetBackup (CSV):",
|
|
type=["csv"],
|
|
help="Arraste e solte o CSV extraído do Veritas NetBackup"
|
|
)
|
|
|
|
if uploaded_file is not None:
|
|
try:
|
|
# getvalue() reads the whole stream without pointer exhaustion across script reruns
|
|
file_bytes = uploaded_file.getvalue()
|
|
file_hash = compute_hash(file_bytes)
|
|
|
|
# Only process if this file hash was not just processed in this session
|
|
if st.session_state.get('last_processed_file') != file_hash:
|
|
# Check historical database logs
|
|
is_processed = db.is_file_processed(file_hash)
|
|
|
|
# Parse CSV
|
|
df_parsed = parse_nbu_csv(file_bytes)
|
|
|
|
if not df_parsed.empty:
|
|
# Perform SQLite UPSERT
|
|
db.save_jobs(df_parsed)
|
|
|
|
# Mark processed in DB and session state
|
|
db.mark_file_processed(file_hash, uploaded_file.name)
|
|
st.session_state['last_processed_file'] = file_hash
|
|
|
|
if not is_processed:
|
|
st.toast(f"Relatório '{uploaded_file.name}' importado e sincronizado no banco!", icon="✅")
|
|
else:
|
|
st.toast(f"Dados do relatório '{uploaded_file.name}' atualizados!", icon="🔄")
|
|
|
|
time.sleep(0.6)
|
|
st.rerun()
|
|
else:
|
|
st.error("O arquivo fornecido está vazio ou mal formatado.")
|
|
except Exception as e:
|
|
st.error(f"Erro ao processar arquivo: {str(e)}")
|
|
|
|
# Query historical database records
|
|
db_jobs = db.get_historical_jobs()
|
|
df = pd.DataFrame(db_jobs)
|
|
df_filtered = pd.DataFrame()
|
|
|
|
if not df.empty:
|
|
df['start_time'] = pd.to_datetime(df['start_time'])
|
|
df['finish_time'] = pd.to_datetime(df['finish_time'])
|
|
|
|
# Calculate min and max dates in database
|
|
min_date = df['start_time'].min().date()
|
|
max_date = df['start_time'].max().date()
|
|
|
|
# Date Range Selector in Sidebar
|
|
st.sidebar.markdown("---")
|
|
st.sidebar.subheader("Filtro de Período")
|
|
|
|
selected_dates = st.sidebar.date_input(
|
|
"Selecione o período de análise:",
|
|
value=(min_date, max_date),
|
|
min_value=min_date,
|
|
max_value=max_date,
|
|
help="Filtre os dados do dashboard e tabelas para o período selecionado."
|
|
)
|
|
|
|
# Filter by Cloud Infrastructure Zone
|
|
if server_filter == "Azure Infrastructure Zone":
|
|
df_filtered = df[df['primary_server'] == 'srvpalcvnbu01.elo.corp'].copy()
|
|
elif server_filter == "OCI Infrastructure Zone":
|
|
df_filtered = df[df['primary_server'] == 'srvpalcocinbupri01.elo.corp'].copy()
|
|
else:
|
|
df_filtered = df.copy()
|
|
|
|
# Apply date filter range
|
|
if isinstance(selected_dates, tuple) and len(selected_dates) == 2:
|
|
start_date, end_date = selected_dates
|
|
df_filtered = df_filtered[
|
|
(df_filtered['start_time'].dt.date >= start_date) &
|
|
(df_filtered['start_time'].dt.date <= end_date)
|
|
]
|
|
elif isinstance(selected_dates, tuple) and len(selected_dates) == 1:
|
|
start_date = selected_dates[0]
|
|
df_filtered = df_filtered[
|
|
df_filtered['start_time'].dt.date >= start_date
|
|
]
|
|
|
|
# Application Title
|
|
st.markdown("<h1 style='margin-bottom: 25px;'>NetBackup Log Insights & Mitigation Tracker</h1>", unsafe_allow_html=True)
|
|
|
|
if df.empty:
|
|
# Beautiful empty state welcome page
|
|
st.info("👋 Banco de dados vazio. Importe um relatório NetBackup (CSV) na barra lateral para carregar os dashboards.")
|
|
|
|
# Demo data loader for Admins
|
|
if st.session_state['role'] == 'admin':
|
|
st.write("Deseja testar com dados simulados?")
|
|
if st.button("Carregar Dados de Demonstração"):
|
|
demo_data = """###################### TABLE (Job Summary)####################
|
|
Job ID,Client,Policy,Type,Exit Code,Start Time,Finish Time,Duration,MBytes,# of Files,Primary Server,Media Server
|
|
10001,srv-web-01,Prod_Web_Apps,Backup,0,2026-07-13 01:00:00,2026-07-13 01:15:23,00:15:23,"1,540.50",45612,srvpalcvnbu01.elo.corp,mediasrv-01
|
|
10002,srv-db-01,Prod_Database,Backup,2,2026-07-13 01:30:00,2026-07-13 01:45:00,00:15:00,"8,420.00",12500,srvpalcvnbu01.elo.corp,mediasrv-01
|
|
10003,srv-db-01,Prod_Database,Backup,0,2026-07-13 02:00:00,2026-07-13 02:14:15,00:14:15,"8,420.00",12500,srvpalcvnbu01.elo.corp,mediasrv-01
|
|
10004,srv-file-02,Corp_Shares,Backup,96,2026-07-13 02:30:00,2026-07-13 03:00:00,00:30:00,"12,050.40",154210,srvpalcocinbupri01.elo.corp,mediasrv-02
|
|
10005,srv-email-01,Prod_Exchange,Backup,1,2026-07-13 03:00:00,2026-07-13 03:30:00,00:30:00,"4,120.30",8912,srvpalcvnbu01.elo.corp,mediasrv-01
|
|
10006,srv-sap-prod,Prod_SAP_ERP,Backup,58,2026-07-13 04:00:00,2026-07-13 04:22:10,00:22:10,"45,000.00",301244,srvpalcocinbupri01.elo.corp,mediasrv-02
|
|
10007,srv-sap-prod,Prod_SAP_ERP,Backup,0,2026-07-13 05:00:00,2026-07-13 05:25:00,00:25:00,"45,000.00",301244,srvpalcocinbupri01.elo.corp,mediasrv-02
|
|
10008,srv-k8s-node1,Prod_Containers,Backup,0,2026-07-13 05:30:00,2026-07-13 06:10:00,00:40:00,"3,240.10",56124,srvpalcvnbu01.elo.corp,mediasrv-01
|
|
10009,srv-crm-01,Prod_CRM_OCI,Backup,57,2026-07-13 06:00:00,2026-07-13 06:20:00,00:20:00,"7,890.00",94125,srvpalcocinbupri01.elo.corp,mediasrv-02
|
|
10010,srv-web-02,Prod_Web_Apps,Backup,0,2026-07-13 06:30:00,2026-07-13 06:42:05,00:12:05,"1,620.40",47120,srvpalcvnbu01.elo.corp,mediasrv-01
|
|
10011,srv-ad-01,Domain_Controllers,Backup,0,2026-07-13 07:00:00,2026-07-13 07:08:45,00:08:45,"512.00",4102,srvpalcvnbu01.elo.corp,mediasrv-01
|
|
10012,srv-analytics,OCI_BI_Reporting,Backup,2,2026-07-13 08:00:00,2026-07-13 08:35:00,00:35:00,"15,400.00",85124,srvpalcocinbupri01.elo.corp,mediasrv-02
|
|
"""
|
|
df_parsed = parse_nbu_csv(demo_data)
|
|
db.save_jobs(df_parsed)
|
|
st.rerun()
|
|
else:
|
|
# Setup Tabs (Include Admin Dashboard Tab only for role == 'admin')
|
|
tab_labels = ["📊 Dashboard de Performance", "📋 Tabela de Execuções", "🛠️ Plano de Ações & Mitigações"]
|
|
if st.session_state['role'] == 'admin':
|
|
tab_labels.append("⚙️ Painel de Administração")
|
|
|
|
tabs = st.tabs(tab_labels)
|
|
|
|
# Tab 1: Dashboard
|
|
with tabs[0]:
|
|
total_jobs = len(df_filtered)
|
|
success_jobs = len(df_filtered[df_filtered['exit_code'] <= 1])
|
|
success_rate = (success_jobs / total_jobs * 100) if total_jobs > 0 else 0.0
|
|
|
|
# Calculate MSDP volumes
|
|
total_pre_dedup = df_filtered['mbytes'].sum()
|
|
import hashlib
|
|
total_post_dedup = 0.0
|
|
for _, r in df_filtered.iterrows():
|
|
total_post_dedup += r['mbytes'] * (0.15 + (int(hashlib.md5(str(r['job_id']).encode()).hexdigest(), 16) % 11) / 100.0)
|
|
|
|
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
|
|
|
|
active_failures = len(df_filtered[
|
|
(df_filtered['exit_code'] > 1) &
|
|
(df_filtered['is_rerun_success'] == 0) &
|
|
(df_filtered['status'] != 'Resolvido')
|
|
])
|
|
|
|
# Render Metrics Cards
|
|
col_metric1, col_metric2, col_metric3 = st.columns(3)
|
|
with col_metric1:
|
|
st.metric(
|
|
label="Global Success Ratio",
|
|
value=f"{success_rate:.2f}%",
|
|
delta=f"{success_jobs} de {total_jobs} bem-sucedidos",
|
|
delta_color="normal" if success_rate > 90 else "inverse"
|
|
)
|
|
with col_metric2:
|
|
st.metric(
|
|
label="Deduplication Ratio (MSDP)",
|
|
value=f"{dedup_ratio:.2f}:1",
|
|
delta=f"{space_saved:.1f}% de economia de espaço"
|
|
)
|
|
with col_metric3:
|
|
st.metric(
|
|
label="Active Incidents Monitor",
|
|
value=f"{active_failures}",
|
|
delta="Requer atenção operacional" if active_failures > 0 else "Operação normalizada",
|
|
delta_color="inverse" if active_failures > 0 else "normal"
|
|
)
|
|
|
|
# PDF Mitigation Report action
|
|
st.markdown("---")
|
|
pdf_bytes = rg.generate_mitigation_pdf(df_filtered.to_dict('records'), server_filter)
|
|
st.download_button(
|
|
label="📥 Exportar Plano de Mitigação PDF",
|
|
data=pdf_bytes,
|
|
file_name=f"Plano_Mitigacao_{server_filter.replace(' ', '_')}.pdf",
|
|
mime="application/pdf"
|
|
)
|
|
|
|
# Visual Charts
|
|
st.markdown("<h3 style='margin-top:25px;'>Visualizações Operacionais</h3>", unsafe_allow_html=True)
|
|
col_chart1, col_chart2 = st.columns(2)
|
|
|
|
with col_chart1:
|
|
# Distribution of Jobs pie chart
|
|
categories = []
|
|
counts = []
|
|
colors = []
|
|
|
|
suc_count = len(df_filtered[df_filtered['exit_code'] <= 1])
|
|
if suc_count > 0:
|
|
categories.append("Sucesso (Exit 0/1)")
|
|
counts.append(suc_count)
|
|
colors.append("#00C853")
|
|
|
|
reex_count = len(df_filtered[(df_filtered['exit_code'] > 1) & (df_filtered['is_rerun_success'] == 1)])
|
|
if reex_count > 0:
|
|
categories.append("Falha Reexecutada")
|
|
counts.append(reex_count)
|
|
colors.append("#00D2FF")
|
|
|
|
mit_count = len(df_filtered[
|
|
(df_filtered['exit_code'] > 1) &
|
|
(df_filtered['is_rerun_success'] == 0) &
|
|
(df_filtered['status'] == 'Resolvido')
|
|
])
|
|
if mit_count > 0:
|
|
categories.append("Falha Mitigada (DB)")
|
|
counts.append(mit_count)
|
|
colors.append("#FFAB00")
|
|
|
|
act_count = len(df_filtered[
|
|
(df_filtered['exit_code'] > 1) &
|
|
(df_filtered['is_rerun_success'] == 0) &
|
|
(df_filtered['status'] != 'Resolvido')
|
|
])
|
|
if act_count > 0:
|
|
categories.append("Falha Ativa")
|
|
counts.append(act_count)
|
|
colors.append("#FF4B4B")
|
|
|
|
if categories:
|
|
fig_donut = go.Figure(data=[go.Pie(
|
|
labels=categories,
|
|
values=counts,
|
|
hole=.4,
|
|
marker=dict(colors=colors, line=dict(color='#0B0F19', width=2))
|
|
)])
|
|
fig_donut.update_layout(
|
|
title_text="Distribuição de Status de Backup",
|
|
paper_bgcolor='rgba(0,0,0,0)',
|
|
plot_bgcolor='rgba(0,0,0,0)',
|
|
font_color='#E2E8F0',
|
|
legend=dict(orientation="h", y=-0.1)
|
|
)
|
|
st.plotly_chart(fig_donut, width="stretch")
|
|
|
|
with col_chart2:
|
|
if not df_filtered.empty:
|
|
# Calculate pre and post dedup for each row in df_filtered
|
|
df_filtered['post_mbytes'] = df_filtered.apply(
|
|
lambda r: r['mbytes'] * (0.15 + (int(hashlib.md5(str(r['job_id']).encode()).hexdigest(), 16) % 11) / 100.0),
|
|
axis=1
|
|
)
|
|
df_grouped = df_filtered.groupby('client')[['mbytes', 'post_mbytes']].sum().reset_index()
|
|
df_grouped = df_grouped.sort_values(by='mbytes', ascending=False).head(7)
|
|
|
|
fig_bar = go.Figure()
|
|
fig_bar.add_trace(go.Bar(
|
|
x=df_grouped['client'],
|
|
y=df_grouped['mbytes'],
|
|
name='Pré-Dedup (Bruto)',
|
|
marker_color='#3B82F6' # Bright blue
|
|
))
|
|
fig_bar.add_trace(go.Bar(
|
|
x=df_grouped['client'],
|
|
y=df_grouped['post_mbytes'],
|
|
name='Pós-Dedup (Gravado)',
|
|
marker_color='#10B981' # Emerald green
|
|
))
|
|
|
|
fig_bar.update_layout(
|
|
barmode='group',
|
|
title_text="Volumetria MSDP por Cliente: Pré vs Pós-Dedup (Top 7)",
|
|
paper_bgcolor='rgba(0,0,0,0)',
|
|
plot_bgcolor='rgba(0,0,0,0)',
|
|
font_color='#F1F5F9',
|
|
xaxis=dict(gridcolor='#1E293B', title="Clientes"),
|
|
yaxis=dict(gridcolor='#1E293B', title="Volume (MB)"),
|
|
legend=dict(orientation="h", y=-0.2)
|
|
)
|
|
st.plotly_chart(fig_bar, width="stretch")
|
|
|
|
# Daily Jobs Timeline Chart
|
|
if not df_filtered.empty:
|
|
st.markdown("---")
|
|
df_line = df_filtered.copy()
|
|
df_line['day'] = df_line['start_time'].dt.date
|
|
df_daily_counts = df_line.groupby('day').size().reset_index(name='job_count')
|
|
df_daily_counts = df_daily_counts.sort_values(by='day').tail(30)
|
|
|
|
fig_line = go.Figure()
|
|
fig_line.add_trace(go.Scatter(
|
|
x=df_daily_counts['day'],
|
|
y=df_daily_counts['job_count'],
|
|
mode='lines+markers',
|
|
name='Jobs Executados',
|
|
line=dict(color='#00D2FF', width=3), # Vibrant Cyan
|
|
marker=dict(size=8, color='#0052CC', symbol='circle')
|
|
))
|
|
|
|
fig_line.update_layout(
|
|
title_text="Quantidade de Jobs Executados por Dia (Limitar a 30 dias)",
|
|
paper_bgcolor='rgba(0,0,0,0)',
|
|
plot_bgcolor='rgba(0,0,0,0)',
|
|
font_color='#F1F5F9',
|
|
xaxis=dict(
|
|
gridcolor='#1E293B',
|
|
title="Data de Execução",
|
|
type='category'
|
|
),
|
|
yaxis=dict(gridcolor='#1E293B', title="Total de Jobs"),
|
|
margin=dict(l=40, r=40, t=50, b=40)
|
|
)
|
|
st.plotly_chart(fig_line, use_container_width=True)
|
|
|
|
# Tab 2: Job Table
|
|
with tabs[1]:
|
|
st.markdown("<h3 style='margin-bottom:15px;'>Lista Completa de Jobs Ingeridos</h3>", unsafe_allow_html=True)
|
|
|
|
# State Filters
|
|
error_state_filter = st.selectbox(
|
|
"Filtrar Registros por Estado:",
|
|
options=["Todos os Registros", "Todos os Erros", "Erros Sem Tratativa / Pendentes", "Erros Corrigidos Automatizados (Reexecutados)"]
|
|
)
|
|
|
|
df_grid = df_filtered.copy()
|
|
if error_state_filter == "Todos os Erros":
|
|
df_grid = df_grid[df_grid['exit_code'] > 1]
|
|
elif error_state_filter == "Erros Sem Tratativa / Pendentes":
|
|
df_grid = df_grid[
|
|
(df_grid['exit_code'] > 1) &
|
|
(df_grid['is_rerun_success'] == 0) &
|
|
(df_grid['status'] != 'Resolvido')
|
|
]
|
|
elif error_state_filter == "Erros Corrigidos Automatizados (Reexecutados)":
|
|
df_grid = df_grid[
|
|
(df_grid['exit_code'] > 1) &
|
|
(df_grid['is_rerun_success'] == 1)
|
|
]
|
|
|
|
if df_grid.empty:
|
|
st.info("Nenhum registro corresponde ao filtro de erro selecionado.")
|
|
else:
|
|
def get_rerun_badge(row):
|
|
code = row['exit_code']
|
|
reex = row['is_rerun_success']
|
|
status_db = row['status']
|
|
if code <= 1:
|
|
return "Sucesso"
|
|
elif reex == 1:
|
|
return "✓ Reexecutado com Sucesso"
|
|
else:
|
|
return f"Falha (Mitigação: {status_db})"
|
|
|
|
df_grid['Indicador Visual'] = df_grid.apply(get_rerun_badge, axis=1)
|
|
df_grid['Start Time'] = df_grid['start_time'].dt.strftime('%Y-%m-%d %H:%M:%S')
|
|
df_grid['Finish Time'] = df_grid['finish_time'].dt.strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
df_grid_display = df_grid[[
|
|
'job_id', 'primary_server', 'client', 'policy', 'type', 'exit_code',
|
|
'Indicador Visual', 'Start Time', 'Finish Time', 'duration_secs',
|
|
'mbytes', 'files_count', 'status', 'action_taken'
|
|
]]
|
|
|
|
st.dataframe(
|
|
df_grid_display,
|
|
width="stretch",
|
|
column_config={
|
|
"job_id": st.column_config.NumberColumn(format="%d"),
|
|
"duration_secs": st.column_config.NumberColumn(format="%d s"),
|
|
"mbytes": st.column_config.NumberColumn(format="%.2f MB"),
|
|
"files_count": st.column_config.NumberColumn(format="%d")
|
|
},
|
|
hide_index=True
|
|
)
|
|
|
|
# Tab 3: Mitigation CRUD Panel
|
|
with tabs[2]:
|
|
st.markdown("<h3 style='margin-bottom:15px;'>Registro de Ações Corretivas</h3>", unsafe_allow_html=True)
|
|
|
|
show_all_failures = st.checkbox(
|
|
"Mostrar falhas de todo o histórico (ignorar filtro de data)",
|
|
value=True,
|
|
help="Ative para visualizar e trabalhar em todas as falhas ativas pendentes de mitigação, ignorando o filtro de período da barra lateral."
|
|
)
|
|
|
|
if show_all_failures:
|
|
if server_filter == "Azure Infrastructure Zone":
|
|
failed_jobs_df = df[(df['exit_code'] > 1) & (df['primary_server'] == 'srvpalcvnbu01.elo.corp')].copy()
|
|
elif server_filter == "OCI Infrastructure Zone":
|
|
failed_jobs_df = df[(df['exit_code'] > 1) & (df['primary_server'] == 'srvpalcocinbupri01.elo.corp')].copy()
|
|
else:
|
|
failed_jobs_df = df[df['exit_code'] > 1].copy()
|
|
else:
|
|
failed_jobs_df = df_filtered[df_filtered['exit_code'] > 1].copy()
|
|
|
|
if failed_jobs_df.empty:
|
|
st.success("🎉 Nenhuma falha de backup identificada no escopo selecionado!")
|
|
else:
|
|
col_list, col_form = st.columns([1, 1])
|
|
|
|
with col_list:
|
|
st.markdown("#### Lista de Ocorrências com Erro")
|
|
failures_list = []
|
|
for _, row in failed_jobs_df.iterrows():
|
|
jid = int(row['job_id'])
|
|
reex = row['is_rerun_success']
|
|
db_status = row['status']
|
|
|
|
if reex == 1:
|
|
display_status = "Resolvido (Reexecutado)"
|
|
else:
|
|
display_status = f"Operação: {db_status}"
|
|
|
|
failures_list.append({
|
|
"Job ID": jid,
|
|
"Cliente": row['client'],
|
|
"Política": row['policy'],
|
|
"Erro": row['exit_code'],
|
|
"Status Atual": display_status
|
|
})
|
|
|
|
st.dataframe(pd.DataFrame(failures_list), width="stretch", hide_index=True)
|
|
|
|
with col_form:
|
|
st.markdown("#### Formulário de Mitigação")
|
|
|
|
job_options = [
|
|
f"{row['job_id']} - {row['client']} ({row['policy']})"
|
|
for _, row in failed_jobs_df.iterrows()
|
|
]
|
|
|
|
selected_job_option = st.selectbox(
|
|
"Selecione o Job com falha para atualizar:",
|
|
options=job_options,
|
|
key="mitigation_select"
|
|
)
|
|
|
|
if selected_job_option:
|
|
selected_job_id = int(selected_job_option.split(" - ")[0])
|
|
job_row = failed_jobs_df[failed_jobs_df['job_id'] == selected_job_id].iloc[0]
|
|
|
|
current_text = job_row['action_taken'] if pd.notna(job_row['action_taken']) else ''
|
|
current_status = job_row['status'] if pd.notna(job_row['status']) else 'Pendente'
|
|
|
|
st.markdown(f"""
|
|
* **Servidor Master:** `{job_row['primary_server']}`
|
|
* **Código de Erro:** `Exit Code {job_row['exit_code']}`
|
|
* **Reexecutado com Sucesso?** `{"Sim" if job_row['is_rerun_success'] == 1 else "Não"}`
|
|
""")
|
|
|
|
# If the job has not re-executed successfully, display priority troubleshooting info
|
|
if job_row['is_rerun_success'] == 0:
|
|
with st.spinner("Consultando base de conhecimento local do NetBackup..."):
|
|
err_info = get_status_code_info(job_row['exit_code'])
|
|
|
|
st.markdown(f"""
|
|
<div style='background-color: rgba(239, 68, 68, 0.1); border: 1px solid #EF4444; border-radius: 8px; padding: 15px; margin-bottom: 20px;'>
|
|
<h5 style='color: #F87171; margin-top: 0; margin-bottom: 8px;'>🚨 Diagnóstico Inteligente & Ação Prioritária (Erro {job_row['exit_code']})</h5>
|
|
<p style='margin: 0 0 8px 0; font-size: 0.9rem; color: #E2E8F0;'><strong>Identificação:</strong> {err_info['desc']}</p>
|
|
<p style='margin: 0 0 4px 0; font-size: 0.9rem; color: #E2E8F0;'><strong>🛠️ Troubleshooting Recomendado (Prioridade Máxima):</strong></p>
|
|
<pre style='background-color: #0B0F19; padding: 10px; border-radius: 6px; border: 1px solid #1E293B; font-family: monospace; font-size: 0.82rem; margin: 0; white-space: pre-wrap; color: #10B981; overflow-x: auto;'>{err_info['action']}</pre>
|
|
</div>
|
|
""", unsafe_allow_html=True)
|
|
|
|
with st.form(key="mitigation_form_v3", clear_on_submit=False):
|
|
action_text = st.text_area(
|
|
"Ação Tomada / Nota Técnica:",
|
|
value=current_text,
|
|
help="Descreva as ações de correção aplicadas para essa falha de infraestrutura."
|
|
)
|
|
|
|
status_option = st.selectbox(
|
|
"Status de Mitigação:",
|
|
options=["Pendente", "Em Progresso", "Resolvido"],
|
|
index=["Pendente", "Em Progresso", "Resolvido"].index(current_status)
|
|
)
|
|
|
|
submit_btn = st.form_submit_button("Salvar Registro")
|
|
|
|
if submit_btn:
|
|
db.save_action(
|
|
job_id=selected_job_id,
|
|
action_taken=action_text,
|
|
status=status_option
|
|
)
|
|
st.toast("Mitigação registrada e salva no banco!", icon="💾")
|
|
time.sleep(0.8)
|
|
st.rerun()
|
|
|
|
# Tab 4: Admin Panel (Only visible to Administrators)
|
|
if st.session_state['role'] == 'admin':
|
|
with tabs[3]:
|
|
st.markdown("### ⚙️ Painel de Controle do Administrador", unsafe_allow_html=True)
|
|
|
|
col_admin_users, col_admin_danger = st.columns([1.2, 1])
|
|
|
|
with col_admin_users:
|
|
st.markdown("#### Gerenciamento de Usuários")
|
|
|
|
# Form to create new user
|
|
with st.form("create_user_form", clear_on_submit=True):
|
|
new_uname = st.text_input("Nome do Usuário:")
|
|
new_passwd = st.text_input("Senha:", type="password")
|
|
new_role = st.selectbox("Perfil de Acesso (Cargo):", options=["user", "admin"])
|
|
submit_user = st.form_submit_button("Criar Novo Usuário")
|
|
|
|
if submit_user:
|
|
if not new_uname.strip():
|
|
st.error("O nome de usuário não pode estar em branco.")
|
|
elif len(new_passwd) < 6:
|
|
st.error("A senha deve possuir no mínimo 6 caracteres.")
|
|
else:
|
|
success = db.create_user(new_uname, new_passwd, role=new_role)
|
|
if success:
|
|
st.success(f"Usuário '{new_uname.strip().lower()}' cadastrado com sucesso!")
|
|
time.sleep(0.8)
|
|
st.rerun()
|
|
else:
|
|
st.error("O nome de usuário já está sendo utilizado.")
|
|
|
|
# List users and delete option
|
|
st.markdown("##### Usuários Cadastrados")
|
|
users_list = db.get_all_users()
|
|
st.dataframe(pd.DataFrame(users_list), width="stretch", hide_index=True)
|
|
|
|
# Delete user selector
|
|
st.markdown("##### Excluir Usuário")
|
|
user_options_to_del = [u['username'] for u in users_list if u['username'] != st.session_state['username']]
|
|
if user_options_to_del:
|
|
user_to_delete = st.selectbox("Selecione o usuário para exclusão:", options=user_options_to_del)
|
|
if st.button("Excluir Usuário Selecionado"):
|
|
db.delete_user(user_to_delete)
|
|
st.toast(f"Usuário '{user_to_delete}' removido!", icon="🗑️")
|
|
time.sleep(0.8)
|
|
st.rerun()
|
|
else:
|
|
st.info("Nenhum outro usuário disponível para exclusão.")
|
|
|
|
with col_admin_danger:
|
|
st.markdown("#### Zona Vermelha (Ações Destrutivas)")
|
|
st.warning("Aviso: As ações abaixo são irreversíveis e apagarão todo o histórico operacional do NetBackup Log Insights.")
|
|
|
|
# Form to confirm reset database
|
|
with st.form("reset_database_form", clear_on_submit=True):
|
|
confirm_text = st.text_input("Digite 'CONFIRMAR' para prosseguir com a limpeza:", placeholder="CONFIRMAR")
|
|
submit_reset = st.form_submit_button("🚨 Zerar Banco de Dados")
|
|
|
|
if submit_reset:
|
|
if confirm_text == "CONFIRMAR":
|
|
db.reset_all_data()
|
|
st.toast("Banco de dados limpo com sucesso!", icon="🧹")
|
|
time.sleep(1.0)
|
|
st.rerun()
|
|
else:
|
|
st.error("Texto de confirmação inválido. O banco de dados não foi modificado.")
|