From daaf433c2a3f5b55f0bcdf1bedfd974dcc679f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Toledo?= Date: Fri, 13 Feb 2026 13:52:04 -0300 Subject: [PATCH] Initial commit: Automation script for Enseg reports --- .gitignore | 7 + README.md | 93 ++++++ gerar_relatorio_enseg_zabbix.py | 501 ++++++++++++++++++++++++++++++++ requirements.txt | 5 + 4 files changed, 606 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 gerar_relatorio_enseg_zabbix.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b73cd14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +*.html +*.xlsx +.venv/ +.env +*copy.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..7aad664 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Manual de Uso - Gerador de Relatório Enseg + +Este script Python automatiza a geração de relatórios de disponibilidade e desempenho para o cliente **Enseg**, utilizando dados do **Zabbix API**. O relatório final é gerado em formato **HTML** com gráficos interativos e layout profissional. + +## Conteúdo do Relatório + +O relatório gerado inclui: +1. **Resumo Executivo**: Visão geral do período. +2. **Notas Extraordinárias** (Opcional): Seção para comunicar manutenções ou falhas sistêmicas. +3. **Dados do Monitoramento**: Período de amostragem e local. +4. **Gráficos de Desempenho**: Gráficos individuais por host mostrando: + * **Status de Ping** (Área Verde): Indica disponibilidade (Up/Down). + * **Perda de Pacotes** (Linha Vermelha): Indica degradação da conexão. +5. **Incidentes**: Tabela com histórico de quedas (Ping inacessível) ou indisponibilidade, incluindo duração e justificativa. + +--- + +## Pré-requisitos + +Certifique-se de ter o **Python 3** instalado. + +### Instalar Dependências + +Abra o terminal na pasta do projeto e execute: + +```bash +pip install -r requirements.txt +``` + +*As bibliotecas principais são: `pyzabbix`, `pandas`, `plotly`, `jinja2`, `requests`.* + +--- + +## Como Executar + +O script é executado via linha de comando (CMD, PowerShell ou Terminal). + +### Comando Básico (Padrão 7 dias) + +```bash +python gerar_relatorio_enseg_zabbix.py --user "seu.usuario" --password "sua.senha" +``` + +### Argumentos Disponíveis + +| Argumento | Obrigatório? | Descrição | Exemplo | +| :--- | :---: | :--- | :--- | +| `--user` | **Sim** | Seu usuário do Zabbix. | `--user "joao.silva"` | +| `--password` | **Sim** | Sua senha do Zabbix. | `--password "123Mudar"` | +| `--days` | Não | Número de dias para análise (Padrão: 7). | `--days 15` | +| `--note` | Não | Nota extraordinária para o relatório (ex: Manutenção). | `--note "Falha na fibra"` | + +### Exemplos de Uso + +**1. Gerar relatório dos últimos 15 dias:** + +```bash +python gerar_relatorio_enseg_zabbix.py --user "joao.goncalves" --password "******" --days 15 +``` + +**2. Adicionar uma Nota de Manutenção:** + +Se houver uma falha conhecida que justifique a falta de dados ou incidentes, use a opção `--note`: + +```bash +python gerar_relatorio_enseg_zabbix.py --user "joao.goncalves" --password "******" --note "Interrupção programada para troca de switch no dia 10/02." +``` +*Isso criará uma caixa amarela de destaque no relatório com o texto informado.* + +--- + +## Filtros Automáticos + +O script já possui filtros configurados internamente para garantir a precisão do relatório: + +* **Grupo de Hosts**: Apenas hosts do grupo `[CLIENTES] - ENSEG`. +* **Exclusões**: Ignora hosts com nomes contendo: + * `Antena` + * `AP` + * `Ramal IP` + * `DVR` + * `Impressora` + * `Painel Solar` +* **Período**: Considera sempre do dia inicial à 00:00 até ontem às 23:59. + +## Solução de Problemas + +* **Erro de Autenticação**: Verifique se usuário e senha estão corretos e se você tem permissão de acesso ao Zabbix. +* **Nenhum dado encontrado**: Verifique se o Zabbix está coletando dados para os hosts do grupo Enseg. +* **Gráfico Vazio**: Pode indicar que o host não tem itens de monitoramento de ICMP Ping (`icmpping`, `icmppingloss`) configurados ou coletando. + +--- +**Desenvolvido por iTGuys** diff --git a/gerar_relatorio_enseg_zabbix.py b/gerar_relatorio_enseg_zabbix.py new file mode 100644 index 0000000..16849ba --- /dev/null +++ b/gerar_relatorio_enseg_zabbix.py @@ -0,0 +1,501 @@ +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 = """ + + + + + Relatório de Disponibilidade - iTGuys + + + +
+ +
+
+ iTGuys +
+ + + + + + + + + +
Classificação da Informação:RELATÓRIO
RESTRITOPágina 1 de 1
+
+ +

RELATÓRIO DE DISPONIBILIDADE

+ + +
RESUMO EXECUTIVO
+

+ Este documento apresenta um resumo da disponibilidade dos equipamentos monitorados no cliente Enseg, + bem como um resumo de eventuais incidentes que causaram indisponibilidade no período apurado. +

+ + +
DADOS DO MONITORAMENTO
+ + + + + + + + + + + + + +
Início da amostragem{{ start_date }}
Fim da amostragem{{ end_date }}
LocalInfraestrutura do Cliente / Datacenter iTGuys
+ + +
GRÁFICOS
+ {% for chart in charts %} +
+ {{ chart | safe }} +
+ {% endfor %} + + +
INCIDENTES
+ {% if alertas_html %} + {{ alertas_html | safe }} + {% endif %} + + {% if note %} +
+ Nota: {{ note }} +
+ {% endif %} + + {% if not alertas_html and not note %} +

Não houve incidentes registrados no período apurado.

+ {% endif %} + +
+ + + """ + + # 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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ef5b0e2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +pandas +pyzabbix +requests +plotly +jinja2