automacao-relatorio-clientes/gerar_relatorio_enseg_zabbi...

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()