502 lines
18 KiB
Python
502 lines
18 KiB
Python
import argparse
|
|
import pandas as pd
|
|
from pyzabbix import ZabbixAPI
|
|
from datetime import datetime, timedelta
|
|
import sys
|
|
import plotly.graph_objects as go
|
|
from plotly.subplots import make_subplots
|
|
import jinja2
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Gerar Relatório Enseg via Zabbix API (HTML)")
|
|
parser.add_argument('--user', required=True, help="Usuário do Zabbix")
|
|
parser.add_argument('--password', required=True, help="Senha do Zabbix")
|
|
parser.add_argument('--days', type=int, default=7, help="Dias para análise (padrão: 7)")
|
|
parser.add_argument('--note', type=str, default="", help="Nota extraordinária para o relatório")
|
|
|
|
args = parser.parse_args()
|
|
|
|
zabbix_url = "https://noc.itguys.com.br"
|
|
|
|
print(f"Conectando ao Zabbix ({zabbix_url})...")
|
|
|
|
try:
|
|
zapi = ZabbixAPI(zabbix_url)
|
|
zapi.login(args.user, args.password)
|
|
print(f"Autenticado com sucesso como: {args.user}")
|
|
except Exception as e:
|
|
print(f"Erro ao conectar/autenticar no Zabbix: {e}")
|
|
sys.exit(1)
|
|
|
|
# Definir intervalo de tempo (Ontem 23:59:59 para trás até 00:00:00 do dia inicial)
|
|
today = datetime.now()
|
|
yesterday = today - timedelta(days=1)
|
|
end_date = yesterday.replace(hour=23, minute=59, second=59, microsecond=0)
|
|
|
|
# Start date ajustado para 00:00:00
|
|
start_date_raw = end_date - timedelta(days=args.days - 1) # -1 porque se dias=7, queremos incluir ontem + 6 dias atras?
|
|
# Se days=7 e ontem=12. 12,11,10,9,8,7,6 (7 dias). 12-6 = 6.
|
|
start_date = start_date_raw.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
time_from = int(start_date.timestamp())
|
|
time_till = int(end_date.timestamp())
|
|
|
|
# Formatar datas para exibição
|
|
periodo_str = f"{start_date.strftime('%d/%m/%Y %H:%M')} a {end_date.strftime('%d/%m/%Y %H:%M')}"
|
|
|
|
print(f"Buscando Grupo de Host '[CLIENTES] - ENSEG'...")
|
|
groups = zapi.hostgroup.get(
|
|
filter={"name": "[CLIENTES] - ENSEG"},
|
|
output=["groupid", "name"]
|
|
)
|
|
|
|
if not groups:
|
|
print("Grupo '[CLIENTES] - ENSEG' não encontrado.")
|
|
sys.exit(0)
|
|
|
|
group_id = groups[0]['groupid']
|
|
|
|
print(f"Buscando hosts do grupo...")
|
|
hosts = zapi.host.get(
|
|
groupids=group_id,
|
|
output=["hostid", "name"],
|
|
searchWildcardsEnabled=True
|
|
)
|
|
|
|
# Filtrar Hosts
|
|
filtered_hosts = []
|
|
ignored_terms = ["antena", "ap ", "ramal ip", "dvr", "impressora", "painel solar"]
|
|
|
|
for h in hosts:
|
|
name_lower = h['name'].lower()
|
|
if any(term in name_lower for term in ignored_terms):
|
|
continue
|
|
filtered_hosts.append(h)
|
|
|
|
hosts = filtered_hosts
|
|
if not hosts:
|
|
print("Nenhum host encontrado após filtros.")
|
|
sys.exit(0)
|
|
|
|
print(f"Analisando {len(hosts)} hosts.")
|
|
host_ids = [h['hostid'] for h in hosts]
|
|
host_map = {h['hostid']: h['name'] for h in hosts}
|
|
|
|
# 1. ALERTAS
|
|
print("Buscando alertas de 'Ping' ou 'Unavailable'...")
|
|
triggers = zapi.trigger.get(
|
|
hostids=host_ids,
|
|
search={"description": "Ping*"},
|
|
output=["triggerid", "description", "priority"],
|
|
searchWildcardsEnabled=True,
|
|
expandDescription=1
|
|
)
|
|
if not triggers:
|
|
triggers = zapi.trigger.get(
|
|
hostids=host_ids,
|
|
search={"description": "*ICMP*"},
|
|
output=["triggerid", "description", "priority"],
|
|
searchWildcardsEnabled=True,
|
|
expandDescription=1
|
|
)
|
|
trigger_ids = [t['triggerid'] for t in triggers]
|
|
|
|
alertas_data = []
|
|
if trigger_ids:
|
|
events = zapi.event.get(
|
|
objectids=trigger_ids,
|
|
time_from=time_from,
|
|
time_till=time_till,
|
|
output="extend",
|
|
select_acknowledges="extend",
|
|
sortfield="clock",
|
|
sortorder="DESC"
|
|
)
|
|
events_problem = [e for e in events if e['value'] == '1']
|
|
|
|
# Maps
|
|
triggers_info = zapi.trigger.get(triggerids=trigger_ids, output=["triggerid", "description"], selectHosts=["hostid", "name"])
|
|
trigger_host_map = {}
|
|
trigger_desc_map = {}
|
|
for t in triggers_info:
|
|
if t['hosts']: trigger_host_map[t['triggerid']] = t['hosts'][0]
|
|
trigger_desc_map[t['triggerid']] = t['description']
|
|
|
|
for e in events_problem:
|
|
obj_id = e['objectid']
|
|
host_info = trigger_host_map.get(obj_id)
|
|
if not host_info: continue
|
|
|
|
start_ts = int(e['clock'])
|
|
start_dt = datetime.fromtimestamp(start_ts)
|
|
r_eventid = e.get('r_eventid')
|
|
duration_str = "Em andamento"
|
|
|
|
if r_eventid and r_eventid != '0':
|
|
try:
|
|
r_evt_api = zapi.event.get(eventids=r_eventid, output=["clock"])
|
|
if r_evt_api:
|
|
duration_str = str(timedelta(seconds=int(r_evt_api[0]['clock']) - start_ts))
|
|
except: pass
|
|
else:
|
|
duration_str = f"{str(timedelta(seconds=int(datetime.now().timestamp()) - start_ts))} (Ativo)"
|
|
|
|
alertas_data.append({
|
|
"Data Inicio": start_dt.strftime("%d/%m/%Y %H:%M:%S"),
|
|
"Host": host_info['name'],
|
|
"Problema": trigger_desc_map.get(obj_id, "Desconhecido"),
|
|
"Duração": duration_str,
|
|
"Justificativa": ""
|
|
})
|
|
|
|
# 2. GRÁFICOS (Histórico)
|
|
print("Gerando gráficos de desempenho...")
|
|
charts_html = []
|
|
|
|
# Keys to fetch (Removido Latência conforme solicitado)
|
|
keys = {
|
|
'icmpping': 'Ping (Status)',
|
|
'icmppingloss': 'Perda (%)'
|
|
}
|
|
|
|
# Buscar itens de todos os hosts de uma vez
|
|
items = zapi.item.get(
|
|
hostids=host_ids,
|
|
search={"key_": "icmpping*"},
|
|
output=["itemid", "hostid", "key_", "name", "value_type"],
|
|
searchWildcardsEnabled=True
|
|
)
|
|
|
|
# Organizar itens por host
|
|
host_items = {h_id: {} for h_id in host_ids}
|
|
for item in items:
|
|
# Simplificar key (remover parametros se houver, ex icmpping[,,,])
|
|
key_simple = item['key_'].split('[')[0]
|
|
if key_simple in keys:
|
|
host_items[item['hostid']][key_simple] = item
|
|
|
|
for host_id in host_ids:
|
|
host_name = host_map[host_id]
|
|
h_items = host_items.get(host_id, {})
|
|
|
|
if not h_items:
|
|
print(f"Skipping {host_name}: itens de ping não encontrados.")
|
|
continue
|
|
|
|
# Buscar histórico
|
|
history_data = {}
|
|
|
|
for k, item in h_items.items():
|
|
hist = zapi.history.get(
|
|
history=item['value_type'],
|
|
itemids=[item['itemid']],
|
|
time_from=time_from,
|
|
time_till=time_till,
|
|
output='extend',
|
|
sortfield='clock',
|
|
sortorder='ASC'
|
|
)
|
|
df = pd.DataFrame(hist)
|
|
if not df.empty:
|
|
# Convert clock to int first, then datetime
|
|
df['clock'] = pd.to_datetime(df['clock'].astype(int), unit='s') - timedelta(hours=3)
|
|
df['value'] = pd.to_numeric(df['value'])
|
|
history_data[k] = df
|
|
|
|
if not history_data:
|
|
continue
|
|
|
|
# Criar Gráfico com Plotly (Tema Escuro)
|
|
fig = make_subplots(specs=[[{"secondary_y": True}]])
|
|
|
|
# 1. Ping Status (Left Y - Area Verde)
|
|
if 'icmpping' in history_data:
|
|
df = history_data['icmpping']
|
|
fig.add_trace(
|
|
go.Scatter(
|
|
x=df['clock'],
|
|
y=df['value'],
|
|
name="Ping (Status)",
|
|
line=dict(color='#00CC00', width=1), # Verde Zabbix
|
|
fill='tozeroy', # Preencher área
|
|
mode='lines'
|
|
),
|
|
secondary_y=False
|
|
)
|
|
|
|
# 2. Perda (Right Y - Linha/Area Vermelha)
|
|
if 'icmppingloss' in history_data:
|
|
df = history_data['icmppingloss']
|
|
# Filtrar valores zerados para não poluir visualmente se quiser, mas Zabbix mostra tudo.
|
|
# O exemplo mostra spikes vermelhos.
|
|
fig.add_trace(
|
|
go.Scatter(
|
|
x=df['clock'],
|
|
y=df['value'],
|
|
name="Perda (%)",
|
|
line=dict(color='red', width=2),
|
|
fill='tozeroy', # Spikes sólidos parecem preenchidos
|
|
mode='lines'
|
|
),
|
|
secondary_y=True
|
|
)
|
|
|
|
fig.update_layout(
|
|
title={
|
|
'text': f"{host_name}: Disponibilidade x Perda de Pacotes",
|
|
'font': {'color': 'white', 'size': 14}
|
|
},
|
|
height=350,
|
|
template="plotly_dark", # Tema Escuro
|
|
paper_bgcolor='rgb(30, 30, 30)', # Fundo container
|
|
plot_bgcolor='rgb(30, 30, 30)', # Fundo plot
|
|
legend=dict(orientation="h", yanchor="bottom", y=-0.2, xanchor="left", x=0, font=dict(color='white')),
|
|
margin=dict(l=40, r=40, t=60, b=40)
|
|
)
|
|
|
|
# Eixo Y Esquerdo (Ping Status 0-1)
|
|
fig.update_yaxes(
|
|
title_text="Status (1=Up, 0=Down)",
|
|
secondary_y=False,
|
|
range=[0, 1.1], # Começa do 0 absoluto para alinhar
|
|
showgrid=True,
|
|
gridcolor='rgb(50, 50, 50)',
|
|
tickfont=dict(color='white'),
|
|
title_font=dict(color='white')
|
|
)
|
|
|
|
# Eixo Y Direito (Perda %)
|
|
fig.update_yaxes(
|
|
title_text="Perda (%)",
|
|
secondary_y=True,
|
|
showgrid=False,
|
|
tickfont=dict(color='red'),
|
|
title_font=dict(color='white'), # Título branco conforme solicitado
|
|
rangemode="tozero" # Começar do zero
|
|
)
|
|
|
|
# Eixo X
|
|
fig.update_xaxes(
|
|
showgrid=True,
|
|
gridcolor='rgb(50, 50, 50)',
|
|
tickfont=dict(color='white')
|
|
)
|
|
|
|
charts_html.append(fig.to_html(full_html=False, include_plotlyjs='cdn'))
|
|
print(f"Gráfico gerado para {host_name}")
|
|
|
|
# GERAÇÃO HTML
|
|
print("Gerando relatório HTML...")
|
|
|
|
# CSS e Template Baseado na Identidade Visual (Azul/Verde/Cinza)
|
|
html_template = """
|
|
<!DOCTYPE html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Relatório de Disponibilidade - iTGuys</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Segoe+UI:wght@400;600;700&display=swap');
|
|
|
|
body {
|
|
font-family: 'Segoe UI', sans-serif;
|
|
color: #333;
|
|
margin: 0;
|
|
padding: 40px;
|
|
background-color: #fff;
|
|
}
|
|
.page-container { max-width: 1200px; margin: 0 auto; }
|
|
|
|
/* Header Top */
|
|
.header-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; border-bottom: 2px solid #ddd; padding-bottom: 20px; }
|
|
.logo-placeholder {
|
|
font-size: 36px;
|
|
font-weight: 800;
|
|
color: #333;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.logo-it { color: #00B0F0; }
|
|
.logo-guys { color: #333; }
|
|
|
|
/* Header Info Box */
|
|
.info-box { border: 1px solid #00B050; border-collapse: collapse; font-size: 12px; font-family: sans-serif; }
|
|
.info-box td { border: 1px solid #00B050; padding: 5px 15px; text-align: center; }
|
|
.lbl-blue { background-color: #DCE6F1; color: #1F497D; font-weight: bold; }
|
|
.lbl-restrito { color: #1F497D; font-style: italic; font-weight: bold; font-size: 14px; }
|
|
|
|
/* Main Title */
|
|
h1.main-title {
|
|
color: #366092;
|
|
font-size: 28px;
|
|
text-transform: uppercase;
|
|
font-weight: 400;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
/* Sections */
|
|
.section-bar {
|
|
background-color: #95B3D7; /* Azul Médio */
|
|
color: white;
|
|
padding: 8px 15px;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
margin-top: 40px;
|
|
margin-bottom: 20px;
|
|
border-left: 10px solid #00B050; /* Verde Accent */
|
|
font-size: 16px;
|
|
}
|
|
|
|
/* Data Monitoring Table */
|
|
.monitoring-data {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-bottom: 20px;
|
|
border-top: 2px solid #366092;
|
|
border-bottom: 2px solid #366092;
|
|
}
|
|
.monitoring-data td { padding: 8px 15px; color: #1F497D; font-size: 14px; }
|
|
.monitoring-data .label {
|
|
width: 200px;
|
|
text-align: right;
|
|
font-weight: bold;
|
|
background-color: #fff;
|
|
}
|
|
.monitoring-data .value {
|
|
background-color: #DCE6F1;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Incidents Table */
|
|
.incidents-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
.incidents-table th {
|
|
background-color: #366092;
|
|
color: white;
|
|
padding: 10px;
|
|
text-align: left;
|
|
text-transform: uppercase;
|
|
font-size: 12px;
|
|
}
|
|
.incidents-table td { border: 1px solid #ccc; padding: 8px; color: #333; }
|
|
.incidents-table tr:nth-child(even) { background-color: #f2f2f2; }
|
|
|
|
/* Charts */
|
|
.chart-wrapper {
|
|
margin-bottom: 30px;
|
|
border: 1px solid #ccc;
|
|
padding: 5px;
|
|
background-color: #000; /* Fundo preto para combinar com o gráfico dark */
|
|
}
|
|
|
|
.summary-text { font-size: 14px; color: #333; margin-bottom: 20px; line-height: 1.5; }
|
|
|
|
.extra-note {
|
|
background-color: #fff3cd;
|
|
border: 1px solid #ffeeba;
|
|
color: #856404;
|
|
padding: 15px;
|
|
margin-top: 15px;
|
|
border-radius: 4px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="page-container">
|
|
<!-- Header -->
|
|
<div class="header-top">
|
|
<div class="logo-placeholder">
|
|
<span class="logo-it">iT</span><span class="logo-guys">Guys</span>
|
|
</div>
|
|
<table class="info-box">
|
|
<tr>
|
|
<td class="lbl-blue">Classificação da Informação:</td>
|
|
<td class="lbl-blue">RELATÓRIO</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="lbl-restrito">RESTRITO</td>
|
|
<td>Página 1 de 1</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<h1 class="main-title">RELATÓRIO DE DISPONIBILIDADE</h1>
|
|
|
|
<!-- Resumo Executivo -->
|
|
<div class="section-bar">RESUMO EXECUTIVO</div>
|
|
<p class="summary-text">
|
|
Este documento apresenta um resumo da disponibilidade dos equipamentos monitorados no cliente <strong>Enseg</strong>,
|
|
bem como um resumo de eventuais incidentes que causaram indisponibilidade no período apurado.
|
|
</p>
|
|
|
|
<!-- Dados do Monitoramento -->
|
|
<div class="section-bar">DADOS DO MONITORAMENTO</div>
|
|
<table class="monitoring-data">
|
|
<tr>
|
|
<td class="label">Início da amostragem</td>
|
|
<td class="value">{{ start_date }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="label">Fim da amostragem</td>
|
|
<td class="value">{{ end_date }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="label">Local</td>
|
|
<td class="value">Infraestrutura do Cliente / Datacenter iTGuys</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<!-- Gráficos -->
|
|
<div class="section-bar">GRÁFICOS</div>
|
|
{% for chart in charts %}
|
|
<div class="chart-wrapper">
|
|
{{ chart | safe }}
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<!-- Incidentes -->
|
|
<div class="section-bar">INCIDENTES</div>
|
|
{% if alertas_html %}
|
|
{{ alertas_html | safe }}
|
|
{% endif %}
|
|
|
|
{% if note %}
|
|
<div class="extra-note">
|
|
<strong>Nota:</strong> {{ note }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if not alertas_html and not note %}
|
|
<p>Não houve incidentes registrados no período apurado.</p>
|
|
{% endif %}
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# Criar DataFrame de Alertas para HTML (aplicando classes do pandas)
|
|
df_alertas = pd.DataFrame(alertas_data)
|
|
alertas_html = ""
|
|
if not df_alertas.empty:
|
|
# Pandas to_html com classes, mas sem border hardcoded
|
|
alertas_html = df_alertas.to_html(index=False, classes='incidents-table', border=0)
|
|
|
|
template = jinja2.Template(html_template)
|
|
html_content = template.render(
|
|
start_date=start_date.strftime('%d/%m/%Y %H:%M'),
|
|
end_date=end_date.strftime('%d/%m/%Y %H:%M'),
|
|
periodo=periodo_str,
|
|
alertas_html=alertas_html,
|
|
charts=charts_html,
|
|
note=args.note
|
|
)
|
|
|
|
filename = f"Relatorio_Enseg_{datetime.now().strftime('%Y-%m-%d')}.html"
|
|
with open(filename, "w", encoding="utf-8") as f:
|
|
f.write(html_content)
|
|
|
|
print(f"Relatório gerado com sucesso: {filename}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|