Initial commit: Automation script for Enseg reports
This commit is contained in:
commit
daaf433c2a
|
|
@ -0,0 +1,7 @@
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.html
|
||||||
|
*.xlsx
|
||||||
|
.venv/
|
||||||
|
.env
|
||||||
|
*copy.py
|
||||||
|
|
@ -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**
|
||||||
|
|
@ -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 = """
|
||||||
|
<!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()
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
pandas
|
||||||
|
pyzabbix
|
||||||
|
requests
|
||||||
|
plotly
|
||||||
|
jinja2
|
||||||
Loading…
Reference in New Issue