824 lines
38 KiB
Python
824 lines
38 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 urllib.request
|
|
import urllib.parse
|
|
import re
|
|
|
|
def search_netbackup_code_online(code):
|
|
"""
|
|
Crawls DuckDuckGo HTML search page to pull standard Veritas troubleshooting steps
|
|
for the specified exit code. Runs with a strict timeout and fallback mechanism.
|
|
"""
|
|
try:
|
|
query = f"veritas netbackup status code {code} explanation solution"
|
|
url = "https://html.duckduckgo.com/html/?q=" + urllib.parse.quote(query)
|
|
req = urllib.request.Request(
|
|
url,
|
|
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
|
|
)
|
|
with urllib.request.urlopen(req, timeout=5) as response:
|
|
html = response.read().decode('utf-8', errors='ignore')
|
|
|
|
snippets = re.findall(r'<a class="result__snippet"[^>]*>(.*?)</a>', html, re.DOTALL)
|
|
if snippets:
|
|
cleaned = []
|
|
for s in snippets[:2]:
|
|
clean = re.sub(r'<[^>]*>', '', s)
|
|
clean = clean.replace('"', '"').replace('&', '&').replace('<', '<').replace('>', '>')
|
|
cleaned.append(clean.strip())
|
|
return "\n\n".join(cleaned)
|
|
except Exception as e:
|
|
return f"Não foi possível consultar a internet para obter informações suplementares: {str(e)}"
|
|
return "Nenhum detalhe extra encontrado na busca rápida."
|
|
|
|
def get_status_code_info(code):
|
|
"""
|
|
Aggregates local expert system knowledge with real-time web crawler lookups.
|
|
"""
|
|
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
|
|
|
|
desc = ""
|
|
action = ""
|
|
|
|
if code_int in local_dict:
|
|
desc = local_dict[code_int]["desc"]
|
|
action = local_dict[code_int]["action"]
|
|
|
|
# Query online search to enrich/fallback
|
|
online_details = search_netbackup_code_online(code_int)
|
|
|
|
if not desc:
|
|
desc = f"Código de status {code_int} do NetBackup"
|
|
|
|
if online_details and not online_details.startswith("Não foi possível"):
|
|
if action:
|
|
action = f"{action}\\n\\n🔍 Detalhes de Análise Online Suplementar:\\n{online_details}"
|
|
else:
|
|
action = f"Ação Recomendada (Coletada Online):\\n{online_details}"
|
|
elif online_details.startswith("Não foi possível") and not action:
|
|
action = f"Ação Recomendada:\\nInvestigue os logs do NetBackup (Activity Monitor) para este código de erro.\\n({online_details})"
|
|
|
|
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'])
|
|
|
|
# Filter by Cloud Infrastructure Zone
|
|
if server_filter == "Azure Infrastructure Zone":
|
|
df_filtered = df[df['primary_server'] == 'srvpalcvnbu01.elo.corp']
|
|
elif server_filter == "OCI Infrastructure Zone":
|
|
df_filtered = df[df['primary_server'] == 'srvpalcocinbupri01.elo.corp']
|
|
else:
|
|
df_filtered = df
|
|
|
|
# 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")
|
|
|
|
# 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)
|
|
|
|
failed_jobs_df = df_filtered[df_filtered['exit_code'] > 1]
|
|
|
|
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("Buscando explicação do código de erro na base de conhecimento online..."):
|
|
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.")
|