feat: add offline PDF status code lookup, date range filter, and daily jobs timeline line chart

This commit is contained in:
Silas Brito
2026-07-13 21:01:40 -03:00
parent f59f0ea732
commit 818f7b7b06
5 changed files with 18530 additions and 54 deletions
+236 -52
View File
@@ -8,69 +8,159 @@ import report_gen as rg
import time
import io
import os
import urllib.request
import urllib.parse
import re
import json
def search_netbackup_code_online(code):
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():
"""
Crawls DuckDuckGo HTML search page to pull standard Veritas troubleshooting steps
for the specified exit code. Runs with a strict timeout and fallback mechanism.
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:
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')
import pypdf
import re
reader = pypdf.PdfReader(PDF_FILE)
full_text_list = []
for page in reader.pages:
text = page.extract_text()
if text:
full_text_list.append(text)
full_text = "\n".join(full_text_list)
# Parse status codes
pattern = re.compile(r'NetBackup\s*status\s*code\s*:\s*(\d+)', re.IGNORECASE)
matches = list(pattern.finditer(full_text))
parsed = {}
bullets = ['', '-', '*', '']
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)
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('&quot;', '"').replace('&amp;', '&').replace('&lt;', '<').replace('&gt;', '>')
cleaned.append(clean.strip())
return "\n\n".join(cleaned)
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 = re.sub(r'\s+', ' ', desc).strip()
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)
# Get first action
first_action = "No specific recommended action found in the manual."
if lines:
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:
first_action = first_bullet
else:
for line in lines:
if line.endswith(':') and len(line) < 40:
continue
first_action = line
break
parsed[code_num] = {
"code": code_num,
"desc": desc,
"first_action": first_action,
"full_action": action_clean
}
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:
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."
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):
"""
Aggregates local expert system knowledge with real-time web crawler lookups.
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."
"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."
"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."
"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."
"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."
"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>."
"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."
"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."
}
}
@@ -79,27 +169,45 @@ def get_status_code_info(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:
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}"
if pdf_desc:
desc = f"{local_dict[code_int]['desc']} (PDF: {pdf_desc})"
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})"
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
@@ -400,13 +508,42 @@ 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']
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']
df_filtered = df[df['primary_server'] == 'srvpalcocinbupri01.elo.corp'].copy()
else:
df_filtered = df
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)
@@ -593,6 +730,39 @@ else:
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]:
@@ -659,7 +829,21 @@ else:
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]
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!")
@@ -718,7 +902,7 @@ else:
# 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..."):
with st.spinner("Consultando base de conhecimento local do NetBackup..."):
err_info = get_status_code_info(job_row['exit_code'])
st.markdown(f"""