Initial commit - Arthur Gold Standard: Tools, Templates e Documentação
This commit is contained in:
commit
79e99e4768
|
|
@ -0,0 +1,51 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Gemini Agent artifacts (keep .gemini out of repo)
|
||||
.gemini/
|
||||
|
||||
# Zabbix source code (too large, not needed for templates)
|
||||
zabbix-release-7.0/
|
||||
|
||||
# Community templates (can be downloaded separately)
|
||||
community-templates/
|
||||
|
||||
# Temporary/Work files
|
||||
*.tmp
|
||||
*.bak
|
||||
*.log
|
||||
templates_work/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
# 🛡️ Zabbix ITGuys - Gold Collection
|
||||
|
||||
**Repositório Oficial de Ferramentas e Templates de Monitoramento de Alta Performance.**
|
||||
|
||||
> *"Um alerta sem contexto é apenas ruído. Um alerta com solução é uma ferramenta."* — Arthur "O Farol" Mendes
|
||||
|
||||
Este repositório contém a suíte de ferramentas e templates "Gold Standard" desenvolvida pela ITGuys para elevar o nível da observabilidade Zabbix. Aqui focamos em **inteligência acionável**, não apenas em coleta de dados.
|
||||
|
||||
---
|
||||
|
||||
## 🏆 O Padrão Arthur (Gold Standard)
|
||||
|
||||
Todo recurso neste repositório segue rigorosos critérios de qualidade:
|
||||
|
||||
1. **🇧🇷 Localização Total:** Tudo o que o operador vê (Items, Triggers, Graphs) está em **Português do Brasil (PT-BR)**, claro e profissional.
|
||||
2. **🧠 Inteligência Acionável:** Alertas explicam o *motivo*, o *impacto* e a *solução*.
|
||||
3. **🔮 Preditividade:** Uso de funções como `timeleft()` e detecção de anomalias para prevenir incidentes antes que ocorram.
|
||||
4. **✅ Integridade Técnica:** UUIDs v4 válidos, sem dependências quebradas e sintaxe YAML impecável.
|
||||
|
||||
---
|
||||
|
||||
## 🧰 Ferramentas (Toolchain)
|
||||
|
||||
Scripts em Python desenvolvidos para garantir a integridade e qualidade dos templates.
|
||||
|
||||
### 1. `validate_zabbix_template.py` (The Gatekeeper)
|
||||
O validador definitivo para templates Zabbix (YAML).
|
||||
- **Verifica:** Sintaxe YAML, Duplicidade de UUIDs, Referências quebradas (Gráficos -> Itens, Dashboards -> Gráficos).
|
||||
- **Garante:** Conformidade estrita com o padrão UUIDv4.
|
||||
- **Audita:** Descrições em inglês (aviso) para garantir a tradução.
|
||||
|
||||
**Uso:**
|
||||
```bash
|
||||
python validate_zabbix_template.py templates_gold/template_app_pfsense_snmp.yaml
|
||||
```
|
||||
|
||||
### 2. `fix_uuids.py`
|
||||
Utilitário para correção em massa e higienização de templates.
|
||||
- Remove metadados sujos de versões instáveis (Zabbix 8.0).
|
||||
- Regenera UUIDs inválidos mantendo a consistência.
|
||||
|
||||
### 3. `merge_exchange.py`
|
||||
Ferramenta para fundir o melhor de dois mundos: templates oficiais da Zabbix com customizações "Gold" da comunidade.
|
||||
|
||||
---
|
||||
|
||||
## 📂 Estrutura do Repositório
|
||||
|
||||
- **`/templates_gold`**: Templates prontos para produção, validados e traduzidos.
|
||||
- *Exemplo:* `template_app_pfsense_snmp.yaml` (Monitoramento completo de pfSense via SNMP com análises de segurança e preditivas).
|
||||
- **`/community-templates`**: Fonte upstream de templates diversos.
|
||||
- **`/deploy_package`**: Scripts e arquivos para deploy automatizado de agentes e proxies.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Como Contribuir
|
||||
|
||||
1. **Valide sempre:** Antes de commitar qualquer template, execute o `validate_zabbix_template.py`.
|
||||
2. **Traduza:** Não deixe termos como "Incoming Traffic" ou "Discarded packets". Use "Tráfego de Entrada" e "Pacotes Descartados".
|
||||
3. **Contextualize:** Na descrição da trigger, explique o que fazer se o alerta disparar.
|
||||
|
||||
---
|
||||
*Mantido pela equipe de SRE da ITGuys.*
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import json
|
||||
import sys
|
||||
|
||||
INPUT_FILE = "zabbix_hosts_export.json"
|
||||
OUTPUT_FILE = "zabbix_hosts_export_NUCLEAR_CLEAN.json"
|
||||
|
||||
def main():
|
||||
print("="*60)
|
||||
print(" 🧹 LIMPADOR EXTREMO (DEEP CLEANER)")
|
||||
print("="*60)
|
||||
print("ℹ️ Modo Deep Clean: Removendo templates E itens/triggers/painéis.")
|
||||
print(" O objetivo é importar apenas os HOSTS (Casca vazia).")
|
||||
|
||||
print(f"📂 Lendo: {INPUT_FILE}...", end=" ")
|
||||
try:
|
||||
with open(INPUT_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
print("✅ OK")
|
||||
except FileNotFoundError:
|
||||
print("❌ Arquivo não encontrado!")
|
||||
sys.exit(1)
|
||||
|
||||
# Detecta onde estão os hosts
|
||||
hosts_list = []
|
||||
if 'zabbix_export' in data and 'hosts' in data['zabbix_export']:
|
||||
hosts_list = data['zabbix_export']['hosts']
|
||||
elif 'hosts' in data:
|
||||
hosts_list = data['hosts']
|
||||
|
||||
count = 0
|
||||
# Limpeza dentro dos hosts
|
||||
if hosts_list:
|
||||
keys_to_remove = ['templates', 'items', 'discovery_rules', 'httptests', 'triggers', 'graphs', 'dashboards', 'macros']
|
||||
# Adicionei 'macros' só por garantia, embora as vezes seja útil manter.
|
||||
# O erro atual é sobre trigger.
|
||||
|
||||
for host in hosts_list:
|
||||
for key in keys_to_remove:
|
||||
if key in host:
|
||||
# REMOVER A CHAVE COMPLETAMENTE em vez de deixar lista vazia []
|
||||
# Algumas versões do Zabbix não gostam de chaves vazias se não houver nada.
|
||||
del host[key]
|
||||
|
||||
# Limpeza extra: Remover tags vazias ou inventory se necessário
|
||||
# Mas o foco é o erro de trigger.
|
||||
|
||||
# Limpeza GLOBAL (Top Level)
|
||||
# Zabbix export pode ter 'triggers' globais fora dos hosts
|
||||
if 'zabbix_export' in data:
|
||||
if 'triggers' in data['zabbix_export']:
|
||||
print("⚠️ Encontrado bloco global de 'triggers'. Removendo...")
|
||||
del data['zabbix_export']['triggers']
|
||||
|
||||
if 'graphs' in data['zabbix_export']:
|
||||
del data['zabbix_export']['graphs']
|
||||
|
||||
print(f"✨ Hosts limpos profundamente: {count}")
|
||||
print(" (Templates, Itens, Triggers e LLDs removidos TOTALMENTE)")
|
||||
|
||||
print(f"💾 Salvando: {OUTPUT_FILE}...", end=" ")
|
||||
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
print("✅ Sucesso!")
|
||||
|
||||
print("\n👉 Use este arquivo CLEAN no novo Zabbix para importar APENAS os hosts.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; OTIMIZAÇÕES PHP 8.3 PARA ZABBIX 7.0 LTS
|
||||
; Arquivo: /etc/php/8.3/fpm/conf.d/99-zabbix.ini
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
[PHP]
|
||||
memory_limit = 512M
|
||||
max_execution_time = 600
|
||||
max_input_time = 600
|
||||
max_input_vars = 10000
|
||||
post_max_size = 50M
|
||||
upload_max_filesize = 50M
|
||||
date.timezone = America/Sao_Paulo
|
||||
|
||||
; Session (usando Redis)
|
||||
session.save_handler = redis
|
||||
session.save_path = "tcp://127.0.0.1:6379?auth=SuaSenhaRedisForteAqui123!"
|
||||
|
||||
[opcache]
|
||||
opcache.enable = 1
|
||||
opcache.memory_consumption = 256
|
||||
opcache.interned_strings_buffer = 16
|
||||
opcache.max_accelerated_files = 10000
|
||||
opcache.revalidate_freq = 60
|
||||
opcache.jit = tracing
|
||||
opcache.jit_buffer_size = 128M
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# Zabbix Extras Manual
|
||||
|
||||
Este pacote `extras/` contém ferramentas profissionais para manutenção e monitoramento avançado.
|
||||
|
||||
## 📋 Conteúdo
|
||||
|
||||
### 1. `db_maintenance.sh` (Manutenção de Banco)
|
||||
Otimiza as tabelas de configuração do Zabbix para manter o frontend rápido.
|
||||
* **Instalação**: Copie para `/usr/local/bin/` e dê permissão `chmod +x`.
|
||||
* **Agendamento (Cron)**: Recomenda-se rodar todo domingo às 03:00.
|
||||
```bash
|
||||
0 3 * * 0 /usr/local/bin/db_maintenance.sh
|
||||
```
|
||||
|
||||
### 2. `zabbix_config_backup.sh` (Backup de Config)
|
||||
Gera um dump SQL apenas das configurações (Hosts, Templates, Users), ignorando o histórico pesado.
|
||||
* **Instalação**: Copie para `/usr/local/bin/`.
|
||||
* **Agendamento**: Rodar diariamente.
|
||||
```bash
|
||||
0 5 * * * /usr/local/bin/zabbix_config_backup.sh
|
||||
```
|
||||
|
||||
### 3. Scripts Externos (`externalscripts/`)
|
||||
Copie estes scripts para `/usr/lib/zabbix/externalscripts/` e ajuste o parâmetro `ExternalScripts` no zabbix_server.conf se necessário.
|
||||
|
||||
#### `check_ssl_advanced.sh`
|
||||
Monitora validade de certificados SSL.
|
||||
* **Dependências**: `openssl`
|
||||
* **Item Key no Zabbix**: `check_ssl_advanced.sh[{HOST.CONN},443]`
|
||||
* **Retorno**: Número de dias restantes.
|
||||
|
||||
#### `check_domain.py`
|
||||
Monitora expiração de domínios via WHOIS.
|
||||
* **Dependências**: `pip3 install python-whois`
|
||||
* **Item Key no Zabbix**: `check_domain.py[{HOST.CONN}]` (Assumindo que o host é o dominio)
|
||||
* **Retorno**: Dias restantes.
|
||||
|
||||
### 4. Scripts de Alerta (`alertscripts/`)
|
||||
Copie para `/usr/lib/zabbix/alertscripts/`.
|
||||
|
||||
#### `telegram_graph.py`
|
||||
Envia notificações **HTML ricas** com emojis e gráfico do incidente.
|
||||
|
||||
* **Configuração**: Edite o arquivo para definir `ZABBIX_URL`, `USER`, `PASS` e `TG_BOT_TOKEN`.
|
||||
* **Zabbix Media Type**:
|
||||
* **Type**: Script
|
||||
* **Script name**: `telegram_graph.py`
|
||||
* **Script Parameters**:
|
||||
1. `{ALERT.SENDTO}`
|
||||
2. `{ALERT.SUBJECT}`
|
||||
3. Cole o JSON abaixo (tudo em uma linha só se possível, ou formate conforme a necessidade do seu Zabbix):
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "{EVENT.ID}",
|
||||
"event_name": "{EVENT.NAME}",
|
||||
"event_nseverity": "{EVENT.NSEVERITY}",
|
||||
"event_opdata": "{EVENT.OPDATA}",
|
||||
"event_date": "{EVENT.DATE}",
|
||||
"event_time": "{EVENT.TIME}",
|
||||
"event_status": "{EVENT.STATUS}",
|
||||
"host_name": "{HOST.NAME}",
|
||||
"host_ip": "{HOST.CONN}",
|
||||
"item_id": "{ITEM.ID1}",
|
||||
"item_name": "{ITEM.NAME1}",
|
||||
"item_value": "{ITEM.LASTVALUE1}",
|
||||
"trigger_id": "{TRIGGER.ID}"
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
#!/usr/bin/env python3
|
||||
# Zabbix Alert Script: Rich Telegram Notification
|
||||
# Sends HTML formatted alert + graph image
|
||||
# Input: JSON String from Zabbix
|
||||
# Place in: /usr/lib/zabbix/alertscripts/
|
||||
|
||||
import sys
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
|
||||
# --- CONFIGURATION ---
|
||||
ZABBIX_URL = "http://localhost" # Base URL, no trailing slash
|
||||
ZABBIX_USER = "Admin"
|
||||
ZABBIX_PASS = "zabbix"
|
||||
TG_BOT_TOKEN = "YOUR_BOT_TOKEN"
|
||||
LOG_FILE = "/var/log/zabbix/telegram_bot.log"
|
||||
# ---------------------
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(filename=LOG_FILE, level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s')
|
||||
|
||||
def get_emoji(severity):
|
||||
# Map Zabbix severity (0-5) to Emojis
|
||||
# 0: Not classified, 1: Info, 2: Warning, 3: Average, 4: High, 5: Disaster
|
||||
# Also handles "Recovery" status logic in message builder
|
||||
mapping = {
|
||||
'0': '📓', # Not classified
|
||||
'1': 'ℹ️', # Information
|
||||
'2': '⚠️', # Warning
|
||||
'3': '🟧', # Average
|
||||
'4': '🔥', # High
|
||||
'5': '🛑' # Disaster
|
||||
}
|
||||
return mapping.get(str(severity), '❓')
|
||||
|
||||
def login_zabbix():
|
||||
api_url = f"{ZABBIX_URL}/api_jsonrpc.php"
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
data = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user.login",
|
||||
"params": {
|
||||
"username": ZABBIX_USER,
|
||||
"password": ZABBIX_PASS
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.post(api_url, json=data, timeout=10)
|
||||
result = resp.json().get('result')
|
||||
if not result:
|
||||
logging.error(f"Zabbix Login Failed: {resp.text}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logging.error(f"Zabbix Login Exception: {e}")
|
||||
return None
|
||||
|
||||
def get_graph_image(session_id, item_id, period=3600):
|
||||
if not item_id:
|
||||
return None
|
||||
|
||||
chart_url = f"{ZABBIX_URL}/chart3.php"
|
||||
cookies = {'zbx_session': session_id}
|
||||
params = {
|
||||
'items[0][itemid]': item_id,
|
||||
'items[0][drawtype]': 5, # Gradient
|
||||
'items[0][color]': 'FF5555',
|
||||
'period': period,
|
||||
'width': 600,
|
||||
'height': 200,
|
||||
'legend': 1
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.get(chart_url, params=params, cookies=cookies, timeout=15)
|
||||
if resp.status_code == 200 and 'image' in resp.headers.get('Content-Type', ''):
|
||||
return resp.content
|
||||
logging.warning(f"Failed to fetch graph. Status: {resp.status_code}")
|
||||
except Exception as e:
|
||||
logging.error(f"Graph Fetch Exception: {e}")
|
||||
return None
|
||||
|
||||
def send_telegram(chat_id, message_html, image_data=None):
|
||||
base_url = f"https://api.telegram.org/bot{TG_BOT_TOKEN}"
|
||||
|
||||
try:
|
||||
# 1. Send Text
|
||||
# Note: If image is present, usually nicer to send image with caption,
|
||||
# but captions have length limits (1024 chars).
|
||||
# Safest is to send text first, then image.
|
||||
|
||||
# However, for a cleaner look ("Option A"), we want text + image.
|
||||
# Let's try sending Photo with Caption if text is short enough,
|
||||
# otherwise split.
|
||||
|
||||
if image_data and len(message_html) < 1000:
|
||||
url_photo = f"{base_url}/sendPhoto"
|
||||
files = {'photo': ('graph.png', image_data, 'image/png')}
|
||||
data = {'chat_id': chat_id, 'caption': message_html, 'parse_mode': 'HTML'}
|
||||
resp = requests.post(url_photo, data=data, files=files, timeout=15)
|
||||
else:
|
||||
# Send Message
|
||||
url_msg = f"{base_url}/sendMessage"
|
||||
data = {'chat_id': chat_id, 'text': message_html, 'parse_mode': 'HTML'}
|
||||
resp = requests.post(url_msg, data=data, timeout=15)
|
||||
|
||||
# Send Image separately if exists
|
||||
if image_data:
|
||||
url_photo = f"{base_url}/sendPhoto"
|
||||
files = {'photo': ('graph.png', image_data, 'image/png')}
|
||||
requests.post(url_photo, data={'chat_id': chat_id}, files=files, timeout=15)
|
||||
|
||||
if resp.status_code != 200:
|
||||
logging.error(f"Telegram Send Failed: {resp.text}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Telegram Exception: {e}")
|
||||
|
||||
def format_message(data):
|
||||
# Extract data with safe defaults
|
||||
status = data.get('event_status', 'PROBLEM') # PROBLEM or RESOLVED
|
||||
severity = data.get('event_nseverity', '0')
|
||||
event_name = data.get('event_name', 'Unknown Event')
|
||||
host_name = data.get('host_name', 'Unknown Host')
|
||||
host_ip = data.get('host_ip', '')
|
||||
event_time = f"{data.get('event_date', '')} {data.get('event_time', '')}"
|
||||
item_val = data.get('item_value', 'N/A')
|
||||
event_id = data.get('event_id', '')
|
||||
trigger_id = data.get('trigger_id', '')
|
||||
opdata = data.get('event_opdata', '')
|
||||
|
||||
emoji = get_emoji(severity)
|
||||
|
||||
# Template V2 - Action Oriented
|
||||
if status == 'RESOLVED':
|
||||
header = f"✅ <b>RESOLVED: {event_name}</b>"
|
||||
# More clean/concise for resolved
|
||||
msg = f"{header}\n\n"
|
||||
msg += f"📍 <b>Host:</b> {host_name}\n"
|
||||
msg += f"⏱️ <b>Time:</b> {event_time}\n"
|
||||
else:
|
||||
header = f"{emoji} <b>{event_name}</b>"
|
||||
|
||||
msg = f"{header}\n\n"
|
||||
msg += f"📍 <b>Host:</b> {host_name} ({host_ip})\n"
|
||||
|
||||
# Prefer OpData if available (shows full context like "CPU: 99% > 90%")
|
||||
# Otherwise show raw Value
|
||||
if opdata:
|
||||
msg += f"📊 <b>Dados:</b> {opdata}\n"
|
||||
else:
|
||||
msg += f"📉 <b>Valor:</b> <code>{item_val}</code>\n"
|
||||
|
||||
msg += f"\n<b>🛠️ AÇÕES RÁPIDAS:</b>\n"
|
||||
|
||||
# Action Links
|
||||
if event_id and trigger_id:
|
||||
link_event = f"{ZABBIX_URL}/tr_events.php?triggerid={trigger_id}&eventid={event_id}"
|
||||
link_ack = f"{ZABBIX_URL}/zabbix.php?action=acknowledge.edit&eventids[]={event_id}"
|
||||
|
||||
msg += f"🔴 <a href=\"{link_event}\">Ver Evento no Zabbix</a>\n"
|
||||
msg += f"🛡️ <a href=\"{link_ack}\">Reconhecer (Ack)</a>"
|
||||
|
||||
return msg
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Zabbix sends parameters:
|
||||
# $1 = SendTo (Chat ID)
|
||||
# $2 = Subject (Ignored in this version, we build our own)
|
||||
# $3 = Message (Expects JSON String)
|
||||
|
||||
if len(sys.argv) < 4:
|
||||
logging.error("Missing arguments. Usage: script.py <chat_id> <subject> <json_data>")
|
||||
print("Usage: telegram_graph.py <chat_id> <subject> <json_data>")
|
||||
sys.exit(1)
|
||||
|
||||
chat_id = sys.argv[1]
|
||||
# subject = sys.argv[2] # Unused
|
||||
raw_data = sys.argv[3]
|
||||
|
||||
try:
|
||||
data = json.loads(raw_data)
|
||||
except json.JSONDecodeError:
|
||||
logging.error(f"Invalid JSON received: {raw_data}")
|
||||
# Fallback: Send raw text if JSON fails
|
||||
send_telegram(chat_id, f"⚠️ <b>JSON Error</b>\n{raw_data}")
|
||||
sys.exit(1)
|
||||
|
||||
# Format HTML
|
||||
message_html = format_message(data)
|
||||
|
||||
# Fetch Graph
|
||||
graph_img = None
|
||||
item_id = data.get('item_id')
|
||||
|
||||
# Only fetch graph for "PROBLEM"
|
||||
if data.get('event_status') != 'RESOLVED' and item_id:
|
||||
token = login_zabbix()
|
||||
if token:
|
||||
graph_img = get_graph_image(token, item_id)
|
||||
|
||||
# Send
|
||||
send_telegram(chat_id, message_html, graph_img)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
#!/bin/bash
|
||||
# Zabbix DB Maintenance Script
|
||||
# Objetivo: Manter tabelas de configuração e eventos rápidas
|
||||
# Requer: sudo e acesso ao usuário postgres
|
||||
|
||||
LOG_FILE="/var/log/zabbix/db_maintenance.log"
|
||||
DB_NAME="zabbix"
|
||||
|
||||
echo "[$(date)] Iniciando manutenção do banco Zabbix..." >> $LOG_FILE
|
||||
|
||||
# Lista de tabelas críticas para o frontend e cache
|
||||
TABLES=(
|
||||
"items"
|
||||
"triggers"
|
||||
"functions"
|
||||
"events"
|
||||
"problem"
|
||||
"sessions"
|
||||
"auditlog"
|
||||
"trends"
|
||||
"trends_uint"
|
||||
)
|
||||
|
||||
for TABLE in "${TABLES[@]}"; do
|
||||
echo " -> Otimizando tabela: $TABLE" >> $LOG_FILE
|
||||
# Usando vacuumdb para simplicidade (parte do client postgresql)
|
||||
# -z = analyze, -v = verbose, -t = table
|
||||
if sudo -u postgres vacuumdb -d $DB_NAME -z -t $TABLE >> $LOG_FILE 2>&1; then
|
||||
echo " OK" >> $LOG_FILE
|
||||
else
|
||||
echo " ERRO" >> $LOG_FILE
|
||||
fi
|
||||
done
|
||||
|
||||
echo "[$(date)] Manutenção concluída." >> $LOG_FILE
|
||||
echo "---------------------------------------------------" >> $LOG_FILE
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env python3
|
||||
# Zabbix External Script: Domain Expiration Check
|
||||
# Usage: check_domain.py <domain>
|
||||
# Requires: pip3 install python-whois
|
||||
|
||||
import sys
|
||||
import whois
|
||||
from datetime import datetime
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("-1") # Missing argument
|
||||
sys.exit(1)
|
||||
|
||||
domain = sys.argv[1]
|
||||
|
||||
try:
|
||||
w = whois.whois(domain)
|
||||
|
||||
# Handle list of dates (some registries return list)
|
||||
expiration_date = w.expiration_date
|
||||
if isinstance(expiration_date, list):
|
||||
expiration_date = expiration_date[0]
|
||||
|
||||
if expiration_date is None:
|
||||
print("-2") # Retrieval failed
|
||||
sys.exit()
|
||||
|
||||
now = datetime.now()
|
||||
delta = expiration_date - now
|
||||
|
||||
print(delta.days)
|
||||
|
||||
except Exception as e:
|
||||
# print(e) # Debug
|
||||
print("-1") # Error
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/bash
|
||||
# Zabbix External Script: Advanced SSL Check
|
||||
# Usage: check_ssl_advanced.sh <hostname> <port>
|
||||
|
||||
SERVER=$1
|
||||
PORT=${2:-443}
|
||||
|
||||
if [ -z "$SERVER" ]; then
|
||||
echo "Usage: $0 <hostname> [port]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Calcular dias para expirar
|
||||
END_DATE=$(echo | openssl s_client -servername "$SERVER" -connect "$SERVER:$PORT" 2>/dev/null | openssl x509 -noout -enddate)
|
||||
|
||||
if [ -z "$END_DATE" ]; then
|
||||
echo "-1" # Error connecting
|
||||
exit
|
||||
fi
|
||||
|
||||
END_DATE_STR=${END_DATE#*=}
|
||||
END_EPOCH=$(date +%s -d "$END_DATE_STR")
|
||||
NOW_EPOCH=$(date +%s)
|
||||
SECONDS_LEFT=$((END_EPOCH - NOW_EPOCH))
|
||||
DAYS_LEFT=$((SECONDS_LEFT / 86400))
|
||||
|
||||
# Retorna número de dias
|
||||
echo "$DAYS_LEFT"
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
#!/bin/bash
|
||||
# Zabbix Config-Only Backup Script
|
||||
# Objetivo: Backup leve (apenas configurações) para recuperação rápida de desastres de config
|
||||
# Ignora tabelas pesadas de histórico (history*, trends*)
|
||||
|
||||
# Configuração
|
||||
DB_NAME="zabbix"
|
||||
DB_USER="zabbix"
|
||||
BACKUP_DIR="/var/backups/zabbix_configs"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
FILENAME="$BACKUP_DIR/zabbix_config_$DATE.sql.gz"
|
||||
|
||||
# Criar diretório se não existir
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Lista de padrões de tabelas para IGNORAR (apenas dados)
|
||||
# Mantemos o schema (-s) de tudo, mas dados (-a) ignoramos estas:
|
||||
EXCLUDE_DATA_FLAGS=""
|
||||
EXCLUDE_TABLES=(
|
||||
"history" "history_uint" "history_str" "history_log" "history_text"
|
||||
"trends" "trends_uint"
|
||||
"auditlog" "events" "problem" "alerts" "acknowledges" "service_alarms"
|
||||
)
|
||||
|
||||
for TABLE in "${EXCLUDE_TABLES[@]}"; do
|
||||
EXCLUDE_DATA_FLAGS="$EXCLUDE_DATA_FLAGS --exclude-table-data=$TABLE"
|
||||
done
|
||||
|
||||
echo "Iniciando backup de configuração..."
|
||||
echo "Arquivo: $FILENAME"
|
||||
|
||||
# pg_dump
|
||||
# -Fp = Plain text (melhor para editar se precisar) ou -Fc (Custom format)
|
||||
# Usando pipe para gzip
|
||||
sudo -u postgres pg_dump $DB_NAME $EXCLUDE_DATA_FLAGS | gzip > "$FILENAME"
|
||||
|
||||
SIZE=$(du -h "$FILENAME" | cut -f1)
|
||||
echo "Backup concluído com sucesso. Tamanho: $SIZE"
|
||||
|
||||
# Manter apenas últimos 30 dias
|
||||
find $BACKUP_DIR -name "zabbix_config_*.sql.gz" -mtime +30 -delete
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
#!/bin/bash
|
||||
# Script de Instalação do Pacote de Otimização Zabbix 7.0 LTS
|
||||
# Executar como ROOT
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "❌ Por favor, execute como root (sudo ./install.sh)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "======================================================="
|
||||
echo "🚀 Iniciando Otimização do Zabbix 7.0 LTS"
|
||||
echo "======================================================="
|
||||
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR="/root/zabbix_backup_$TIMESTAMP"
|
||||
|
||||
echo "📂 Criando diretório de backup: $BACKUP_DIR"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Função para backup e cópia
|
||||
install_config() {
|
||||
SRC=$1
|
||||
DEST=$2
|
||||
if [ -f "$DEST" ]; then
|
||||
echo " ↪ Backup de $DEST..."
|
||||
cp "$DEST" "$BACKUP_DIR/"
|
||||
fi
|
||||
echo " ↪ Instalando $SRC em $DEST..."
|
||||
cp "$SRC" "$DEST"
|
||||
}
|
||||
|
||||
# 1. Zabbix Server
|
||||
echo "🔧 Configurando Zabbix Server..."
|
||||
install_config "zabbix_server.conf" "/etc/zabbix/zabbix_server.conf"
|
||||
|
||||
# 2. PostgreSQL
|
||||
echo "🔧 Configurando PostgreSQL 16..."
|
||||
# Ajuste o caminho se a versão for diferente
|
||||
if [ -d "/etc/postgresql/16/main" ]; then
|
||||
install_config "postgresql.conf" "/etc/postgresql/16/main/postgresql.conf"
|
||||
else
|
||||
echo "⚠️ Diretório PostgreSQL 16 não encontrado. Pulando config automática."
|
||||
echo " Arquivo disponível em: $(pwd)/postgresql.conf"
|
||||
fi
|
||||
|
||||
# 3. Nginx
|
||||
echo "🔧 Configurando Nginx..."
|
||||
install_config "nginx.conf" "/etc/nginx/nginx.conf"
|
||||
install_config "zabbix.conf" "/etc/nginx/conf.d/zabbix.conf"
|
||||
# Remover default se existir
|
||||
[ -f "/etc/nginx/sites-enabled/default" ] && rm "/etc/nginx/sites-enabled/default"
|
||||
|
||||
# 4. PHP
|
||||
echo "🔧 Configurando PHP 8.3..."
|
||||
# Ajuste o caminho se a versão for diferente
|
||||
if [ -d "/etc/php/8.3/fpm" ]; then
|
||||
install_config "99-zabbix.ini" "/etc/php/8.3/fpm/conf.d/99-zabbix.ini"
|
||||
install_config "zabbix_fpm.conf" "/etc/php/8.3/fpm/pool.d/zabbix.conf"
|
||||
else
|
||||
echo "⚠️ Diretório PHP 8.3 não encontrado. Pulando config automática."
|
||||
fi
|
||||
|
||||
# 5. Redis
|
||||
echo "🔧 Configurando Redis..."
|
||||
install_config "redis.conf" "/etc/redis/redis.conf"
|
||||
chown redis:redis /etc/redis/redis.conf
|
||||
chmod 640 /etc/redis/redis.conf
|
||||
|
||||
# 6. Database Setup
|
||||
echo "🗄️ ATENÇÃO: Configuração de Banco de Dados"
|
||||
echo " Para aplicar as otimizações TimescaleDB, execute manualmente:"
|
||||
echo " sudo -u postgres psql -d zabbix -f timescaledb_setup.sql"
|
||||
|
||||
# Finalização
|
||||
echo "======================================================="
|
||||
echo "✅ Arquivos copiados com sucesso!"
|
||||
echo "🔄 Lembre-se de ajustar as SENHAS nos arquivos:"
|
||||
echo " - /etc/zabbix/zabbix_server.conf (DBPassword)"
|
||||
echo " - /etc/redis/redis.conf (requirepass)"
|
||||
echo " - /etc/php/8.3/fpm/conf.d/99-zabbix.ini (session.save_path)"
|
||||
echo ""
|
||||
echo "Para aplicar as alterações, reinicie os serviços:"
|
||||
echo "systemctl restart postgresql redis-server php8.3-fpm nginx zabbix-server"
|
||||
echo "======================================================="
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
user www-data;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
include /etc/nginx/modules-enabled/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 768;
|
||||
# multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
|
||||
##
|
||||
# Basic Settings
|
||||
##
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
# server_names_hash_bucket_size 64;
|
||||
# server_name_in_redirect off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
##
|
||||
# SSL Settings
|
||||
##
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
##
|
||||
# Logging Settings
|
||||
##
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
##
|
||||
# Gzip Settings
|
||||
##
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
##
|
||||
# Virtual Host Configs
|
||||
##
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
############ CONFIGURAÇÃO OTIMIZADA POSTGRESQL 16 + TIMESCALEDB ############
|
||||
# Otimizado para: 4 núcleos CPU, 8GB RAM
|
||||
# Ambiente: Zabbix 7.0 LTS com ~1000 equipamentos
|
||||
# Features: TimescaleDB Hypertables + Compressão nativa
|
||||
# Objetivo: Máxima performance para time-series e retenção longa
|
||||
############################################################################
|
||||
|
||||
# Adicione ao arquivo: /etc/postgresql/16/main/postgresql.conf
|
||||
|
||||
############################################################################
|
||||
# CONEXÕES
|
||||
############################################################################
|
||||
|
||||
listen_addresses = '*'
|
||||
max_connections = 300
|
||||
superuser_reserved_connections = 3
|
||||
|
||||
############################################################################
|
||||
# MEMÓRIA - OTIMIZADO PARA 8GB RAM (3GB para PostgreSQL)
|
||||
############################################################################
|
||||
|
||||
# Buffer compartilhado (25-30% da RAM disponível para PostgreSQL)
|
||||
shared_buffers = 2GB
|
||||
|
||||
# Cache efetivo (estimativa de memória total disponível para cache)
|
||||
# Inclui cache do SO + shared_buffers
|
||||
effective_cache_size = 4GB
|
||||
|
||||
# Memória para operações de manutenção (VACUUM, CREATE INDEX)
|
||||
maintenance_work_mem = 512MB
|
||||
|
||||
# Memória por operação de sort/hash (conexão individual)
|
||||
work_mem = 32MB
|
||||
|
||||
# Memória para operações de autovacuum
|
||||
autovacuum_work_mem = 256MB
|
||||
|
||||
############################################################################
|
||||
# WAL (Write-Ahead Log) - OTIMIZADO PARA ALTA ESCRITA
|
||||
############################################################################
|
||||
|
||||
# Tamanho do buffer WAL em memória
|
||||
wal_buffers = 16MB
|
||||
|
||||
# Nível mínimo de WAL (minimal = máxima performance sem replicação)
|
||||
wal_level = minimal
|
||||
|
||||
# Tamanhos máximo e mínimo de WAL em disco
|
||||
max_wal_size = 4GB
|
||||
min_wal_size = 1GB
|
||||
|
||||
# Compressão de WAL (reduz I/O de disco)
|
||||
wal_compression = on
|
||||
|
||||
# Método de gravação no disco
|
||||
wal_writer_delay = 200ms
|
||||
|
||||
############################################################################
|
||||
# CHECKPOINTS - REDUZ I/O E AUMENTA PERFORMANCE
|
||||
############################################################################
|
||||
|
||||
# Target de conclusão do checkpoint (0.9 = 90% do intervalo)
|
||||
checkpoint_completion_target = 0.9
|
||||
|
||||
# Intervalo entre checkpoints automáticos
|
||||
checkpoint_timeout = 15min
|
||||
|
||||
# Log de checkpoints (útil para tuning)
|
||||
log_checkpoints = on
|
||||
|
||||
############################################################################
|
||||
# PLANNER E QUERY OPTIMIZATION
|
||||
############################################################################
|
||||
|
||||
# Custo estimado de leitura aleatória em disco
|
||||
# SSD = 1.1, HDD = 4.0
|
||||
random_page_cost = 1.1
|
||||
|
||||
# Custo de leitura sequencial
|
||||
seq_page_cost = 1.0
|
||||
|
||||
# Capacidade de I/O paralelo (SSD)
|
||||
effective_io_concurrency = 200
|
||||
|
||||
# Limite de custo para uso de índice
|
||||
cpu_tuple_cost = 0.01
|
||||
cpu_index_tuple_cost = 0.005
|
||||
cpu_operator_cost = 0.0025
|
||||
|
||||
############################################################################
|
||||
# PARALELISMO - APROVEITA 4 NÚCLEOS
|
||||
############################################################################
|
||||
|
||||
# Máximo de workers para processos em background
|
||||
max_worker_processes = 8
|
||||
|
||||
# Workers por query paralela
|
||||
max_parallel_workers_per_gather = 2
|
||||
|
||||
# Total de workers paralelos simultâneos
|
||||
max_parallel_workers = 4
|
||||
|
||||
# Mínimo de linhas para considerar paralelismo
|
||||
min_parallel_table_scan_size = 8MB
|
||||
min_parallel_index_scan_size = 512kB
|
||||
|
||||
############################################################################
|
||||
# AUTOVACUUM - CRÍTICO PARA ZABBIX COM TIME-SERIES
|
||||
############################################################################
|
||||
|
||||
# Habilitar autovacuum (OBRIGATÓRIO)
|
||||
autovacuum = on
|
||||
|
||||
# Número de processos autovacuum simultâneos
|
||||
autovacuum_max_workers = 4
|
||||
|
||||
# Intervalo entre varreduras de autovacuum
|
||||
autovacuum_naptime = 10s
|
||||
|
||||
# Thresholds agressivos para Zabbix (muitas escritas)
|
||||
autovacuum_vacuum_threshold = 50
|
||||
autovacuum_analyze_threshold = 50
|
||||
|
||||
# Scale factors reduzidos (mais agressivo)
|
||||
autovacuum_vacuum_scale_factor = 0.01
|
||||
autovacuum_analyze_scale_factor = 0.005
|
||||
|
||||
# Custo e delay do autovacuum
|
||||
autovacuum_vacuum_cost_delay = 10ms
|
||||
autovacuum_vacuum_cost_limit = 1000
|
||||
|
||||
# Prevenir wraparound
|
||||
autovacuum_freeze_max_age = 200000000
|
||||
|
||||
############################################################################
|
||||
# VACUUM E MANUTENÇÃO
|
||||
############################################################################
|
||||
|
||||
# Age máximo para vacuum de prevenção
|
||||
vacuum_freeze_min_age = 50000000
|
||||
vacuum_freeze_table_age = 150000000
|
||||
|
||||
# Custo de vacuum manual
|
||||
vacuum_cost_delay = 0
|
||||
vacuum_cost_limit = 200
|
||||
|
||||
############################################################################
|
||||
# OTIMIZAÇÕES ESPECÍFICAS PARA ZABBIX/TIME-SERIES
|
||||
############################################################################
|
||||
|
||||
# Commit assíncrono (aceita pequeno risco para MÁXIMA performance)
|
||||
# ATENÇÃO: Em caso de crash, pode perder até 1 segundo de dados
|
||||
synchronous_commit = off
|
||||
|
||||
# Desabilita full page writes (maior risco, muito mais performance)
|
||||
# RECOMENDADO apenas com filesystem confiável (ext4, xfs, zfs)
|
||||
full_page_writes = off
|
||||
|
||||
# Compressão de TOAST
|
||||
default_toast_compression = lz4
|
||||
|
||||
############################################################################
|
||||
# TIMESCALEDB - EXTENSÃO PARA TIME-SERIES
|
||||
############################################################################
|
||||
|
||||
# Preload da extensão TimescaleDB
|
||||
shared_preload_libraries = 'timescaledb,pg_stat_statements'
|
||||
|
||||
# Configurações TimescaleDB
|
||||
timescaledb.max_background_workers = 8
|
||||
|
||||
############################################################################
|
||||
# LOGGING - ANÁLISE E TROUBLESHOOTING
|
||||
############################################################################
|
||||
|
||||
# Destino dos logs
|
||||
log_destination = 'stderr'
|
||||
logging_collector = on
|
||||
log_directory = 'log'
|
||||
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
|
||||
log_rotation_age = 1d
|
||||
log_rotation_size = 100MB
|
||||
|
||||
# Formato e informações dos logs
|
||||
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
|
||||
log_timezone = 'America/Sao_Paulo'
|
||||
|
||||
# Logs de queries lentas (> 1 segundo)
|
||||
log_min_duration_statement = 1000
|
||||
|
||||
# Logs de conexões e locks
|
||||
log_connections = on
|
||||
log_disconnections = on
|
||||
log_lock_waits = on
|
||||
|
||||
# Log de tabelas temporárias grandes
|
||||
log_temp_files = 0
|
||||
|
||||
# Log de comandos DDL
|
||||
log_statement = 'ddl'
|
||||
|
||||
# Autovacuum logging
|
||||
log_autovacuum_min_duration = 0
|
||||
|
||||
############################################################################
|
||||
# CONFIGURAÇÕES DE LOCALE E ENCODING
|
||||
############################################################################
|
||||
|
||||
# Encoding padrão (UTF-8)
|
||||
client_encoding = UTF8
|
||||
|
||||
# Locale
|
||||
lc_messages = 'pt_BR.UTF-8'
|
||||
lc_monetary = 'pt_BR.UTF-8'
|
||||
lc_numeric = 'pt_BR.UTF-8'
|
||||
lc_time = 'pt_BR.UTF-8'
|
||||
|
||||
# Timezone
|
||||
timezone = 'America/Sao_Paulo'
|
||||
|
||||
# Comportamento de strings vazias
|
||||
default_text_search_config = 'pg_catalog.portuguese'
|
||||
|
||||
############################################################################
|
||||
# SEGURANÇA E LIMITES
|
||||
############################################################################
|
||||
|
||||
# Timeout de statement (evita queries runaway)
|
||||
statement_timeout = 0
|
||||
|
||||
# Timeout de lock
|
||||
lock_timeout = 0
|
||||
|
||||
# Timeout de idle in transaction
|
||||
idle_in_transaction_session_timeout = 0
|
||||
|
||||
############################################################################
|
||||
# ESTATÍSTICAS
|
||||
############################################################################
|
||||
|
||||
# Nível de estatísticas coletadas
|
||||
track_activities = on
|
||||
track_counts = on
|
||||
track_io_timing = on
|
||||
track_functions = all
|
||||
|
||||
# Estatísticas de queries (requer pg_stat_statements)
|
||||
pg_stat_statements.max = 10000
|
||||
pg_stat_statements.track = all
|
||||
|
||||
############################################################################
|
||||
# OUTROS
|
||||
############################################################################
|
||||
|
||||
# Número de buffers de disco sujos antes de fsync
|
||||
bgwriter_delay = 200ms
|
||||
bgwriter_lru_maxpages = 100
|
||||
bgwriter_lru_multiplier = 2.0
|
||||
|
||||
# JIT compilation (PostgreSQL 11+)
|
||||
jit = on
|
||||
jit_above_cost = 100000
|
||||
jit_inline_above_cost = 500000
|
||||
jit_optimize_above_cost = 500000
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
################################################################################
|
||||
# CONFIGURAÇÃO REDIS OTIMIZADA PARA ZABBIX
|
||||
################################################################################
|
||||
|
||||
bind 127.0.0.1 ::1
|
||||
port 6379
|
||||
requirepass SuaSenhaRedisForteAqui123!
|
||||
protected-mode yes
|
||||
|
||||
# Timeout & Keepalive
|
||||
timeout 300
|
||||
tcp-keepalive 300
|
||||
|
||||
# Persistência (RDB)
|
||||
save 900 1
|
||||
save 300 10
|
||||
save 60 10000
|
||||
stop-writes-on-bgsave-error yes
|
||||
rdbcompression yes
|
||||
rdbchecksum yes
|
||||
dbfilename dump.rdb
|
||||
dir /var/lib/redis
|
||||
|
||||
# Memória (1GB dedicado)
|
||||
maxmemory 1gb
|
||||
maxmemory-policy allkeys-lru
|
||||
|
||||
# Logging
|
||||
loglevel notice
|
||||
logfile /var/log/redis/redis-server.log
|
||||
|
||||
# Clientes
|
||||
maxclients 1000
|
||||
|
||||
# Slow log
|
||||
slowlog-log-slower-than 10000
|
||||
slowlog-max-len 128
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
-- ############################################################################
|
||||
-- SCRIPT DE CONFIGURAÇÃO TIMESCALEDB PARA ZABBIX 7.0 LTS
|
||||
-- Execute como usuário postgres: psql -U postgres -d zabbix -f timescaledb_setup.sql
|
||||
-- ############################################################################
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
|
||||
|
||||
-- History
|
||||
SELECT create_hypertable('history', 'clock', chunk_time_interval => 86400, if_not_exists => TRUE, migrate_data => TRUE);
|
||||
SELECT create_hypertable('history_uint', 'clock', chunk_time_interval => 86400, if_not_exists => TRUE, migrate_data => TRUE);
|
||||
SELECT create_hypertable('history_str', 'clock', chunk_time_interval => 86400, if_not_exists => TRUE, migrate_data => TRUE);
|
||||
SELECT create_hypertable('history_log', 'clock', chunk_time_interval => 86400, if_not_exists => TRUE, migrate_data => TRUE);
|
||||
SELECT create_hypertable('history_text', 'clock', chunk_time_interval => 86400, if_not_exists => TRUE, migrate_data => TRUE);
|
||||
|
||||
-- Trends
|
||||
SELECT create_hypertable('trends', 'clock', chunk_time_interval => 2592000, if_not_exists => TRUE, migrate_data => TRUE);
|
||||
SELECT create_hypertable('trends_uint', 'clock', chunk_time_interval => 2592000, if_not_exists => TRUE, migrate_data => TRUE);
|
||||
|
||||
-- Compressão (History: 1 dia, Trends: 7 dias)
|
||||
ALTER TABLE history SET (timescaledb.compress, timescaledb.compress_segmentby = 'itemid', timescaledb.compress_orderby = 'clock DESC');
|
||||
SELECT add_compression_policy('history', INTERVAL '1 day');
|
||||
|
||||
ALTER TABLE history_uint SET (timescaledb.compress, timescaledb.compress_segmentby = 'itemid', timescaledb.compress_orderby = 'clock DESC');
|
||||
SELECT add_compression_policy('history_uint', INTERVAL '1 day');
|
||||
|
||||
ALTER TABLE history_str SET (timescaledb.compress, timescaledb.compress_segmentby = 'itemid', timescaledb.compress_orderby = 'clock DESC');
|
||||
SELECT add_compression_policy('history_str', INTERVAL '1 day');
|
||||
|
||||
ALTER TABLE history_log SET (timescaledb.compress, timescaledb.compress_segmentby = 'itemid', timescaledb.compress_orderby = 'clock DESC');
|
||||
SELECT add_compression_policy('history_log', INTERVAL '1 day');
|
||||
|
||||
ALTER TABLE history_text SET (timescaledb.compress, timescaledb.compress_segmentby = 'itemid', timescaledb.compress_orderby = 'clock DESC');
|
||||
SELECT add_compression_policy('history_text', INTERVAL '1 day');
|
||||
|
||||
ALTER TABLE trends SET (timescaledb.compress, timescaledb.compress_segmentby = 'itemid', timescaledb.compress_orderby = 'clock DESC');
|
||||
SELECT add_compression_policy('trends', INTERVAL '7 days');
|
||||
|
||||
ALTER TABLE trends_uint SET (timescaledb.compress, timescaledb.compress_segmentby = 'itemid', timescaledb.compress_orderby = 'clock DESC');
|
||||
SELECT add_compression_policy('trends_uint', INTERVAL '7 days');
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
upstream zabbix_backend {
|
||||
server unix:/run/php/php8.3-fpm.sock;
|
||||
keepalive 32;
|
||||
keepalive_requests 1000;
|
||||
keepalive_timeout 60s;
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# RATE LIMITING
|
||||
################################################################################
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/s;
|
||||
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
|
||||
limit_req_zone $binary_remote_addr zone=general_limit:10m rate=100r/s;
|
||||
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
|
||||
|
||||
################################################################################
|
||||
# FASTCGI CACHE
|
||||
################################################################################
|
||||
|
||||
fastcgi_cache_path /var/cache/nginx/zabbix
|
||||
levels=1:2
|
||||
keys_zone=zabbix_cache:100m
|
||||
max_size=2g
|
||||
inactive=1d
|
||||
use_temp_path=off;
|
||||
|
||||
fastcgi_cache_key "$scheme$request_method$host$request_uri$cookie_zbx_session";
|
||||
|
||||
################################################################################
|
||||
# HTTP - REDIRECT TO HTTPS
|
||||
################################################################################
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
# ADJUST SERVER NAME HERE
|
||||
server_name zabbix.seudominio.com.br;
|
||||
server_tokens off;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
default_type "text/plain";
|
||||
root /var/www/html;
|
||||
allow all;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# HTTPS - MAIN SERVER
|
||||
################################################################################
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
|
||||
# ADJUST SERVER NAME HERE
|
||||
server_name zabbix.seudominio.com.br;
|
||||
server_tokens off;
|
||||
|
||||
# SSL/TLS - ADJUST PATHS HERE
|
||||
ssl_certificate /etc/nginx/ssl/zabbix.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/zabbix.key;
|
||||
|
||||
# Recommended SSL settings if not managed by Certbot
|
||||
ssl_session_cache shared:ZabbixSSL:50m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# Security Headers
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# General
|
||||
root /usr/share/zabbix;
|
||||
index index.php;
|
||||
access_log /var/log/nginx/zabbix_access.log combined buffer=32k flush=5s;
|
||||
error_log /var/log/nginx/zabbix_error.log warn;
|
||||
|
||||
client_max_body_size 50M;
|
||||
client_body_buffer_size 256k;
|
||||
client_header_buffer_size 4k;
|
||||
large_client_header_buffers 4 16k;
|
||||
|
||||
client_body_timeout 30s;
|
||||
client_header_timeout 30s;
|
||||
send_timeout 60s;
|
||||
limit_conn conn_limit 50;
|
||||
|
||||
# Security Blocks
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
location ~ ^/(conf|app|include|local)/ {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ \.(bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist)$ {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Static Files
|
||||
location ~* \.(jpg|jpeg|gif|png|webp|ico|svg)$ {
|
||||
expires 90d;
|
||||
add_header Cache-Control "public, immutable, no-transform";
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ~* \.(css|js)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ~* \.(woff|woff2|ttf|eot|otf)$ {
|
||||
expires 365d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Zabbix API
|
||||
location /api_jsonrpc.php {
|
||||
limit_req zone=api_limit burst=50 nodelay;
|
||||
fastcgi_no_cache 1;
|
||||
fastcgi_cache_bypass 1;
|
||||
fastcgi_pass zabbix_backend;
|
||||
include fastcgi_params;
|
||||
fastcgi_param HTTPS on;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_buffer_size 64k;
|
||||
fastcgi_buffers 16 32k;
|
||||
fastcgi_busy_buffers_size 64k;
|
||||
fastcgi_read_timeout 600s;
|
||||
fastcgi_send_timeout 600s;
|
||||
}
|
||||
|
||||
# Login Protection
|
||||
location = /index.php {
|
||||
limit_req zone=login_limit burst=3 nodelay;
|
||||
fastcgi_pass zabbix_backend;
|
||||
include fastcgi_params;
|
||||
fastcgi_param HTTPS on;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_buffer_size 32k;
|
||||
fastcgi_buffers 8 16k;
|
||||
fastcgi_busy_buffers_size 32k;
|
||||
fastcgi_read_timeout 300s;
|
||||
fastcgi_no_cache 1;
|
||||
fastcgi_cache_bypass 1;
|
||||
}
|
||||
|
||||
# PHP with FastCGI Cache
|
||||
location ~ \.php$ {
|
||||
limit_req zone=general_limit burst=200 nodelay;
|
||||
try_files $uri =404;
|
||||
fastcgi_pass zabbix_backend;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param HTTPS on;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
|
||||
# Cache configuration
|
||||
fastcgi_cache zabbix_cache;
|
||||
fastcgi_cache_valid 200 301 302 10m;
|
||||
fastcgi_cache_valid 404 1m;
|
||||
fastcgi_cache_valid any 1m;
|
||||
fastcgi_cache_bypass $http_pragma $http_authorization $cookie_zbx_session;
|
||||
fastcgi_no_cache $http_pragma $http_authorization $cookie_zbx_session;
|
||||
add_header X-FastCGI-Cache $upstream_cache_status;
|
||||
|
||||
fastcgi_buffer_size 32k;
|
||||
fastcgi_buffers 16 32k;
|
||||
fastcgi_busy_buffers_size 64k;
|
||||
fastcgi_connect_timeout 60s;
|
||||
fastcgi_send_timeout 300s;
|
||||
fastcgi_read_timeout 300s;
|
||||
}
|
||||
|
||||
# Health Check
|
||||
location /nginx-health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
[zabbix]
|
||||
user = www-data
|
||||
group = www-data
|
||||
|
||||
listen = /run/php/php8.3-fpm.sock
|
||||
listen.owner = www-data
|
||||
listen.group = www-data
|
||||
listen.mode = 0660
|
||||
|
||||
pm = dynamic
|
||||
pm.max_children = 50
|
||||
pm.start_servers = 5
|
||||
pm.min_spare_servers = 5
|
||||
pm.max_spare_servers = 10
|
||||
pm.max_requests = 500
|
||||
|
||||
; Monitoramento
|
||||
pm.status_path = /fpm-status
|
||||
ping.path = /fpm-ping
|
||||
|
||||
; Logging
|
||||
access.log = /var/log/php8.3-fpm/zabbix-access.log
|
||||
slowlog = /var/log/php8.3-fpm/zabbix-slow.log
|
||||
request_slowlog_timeout = 10s
|
||||
request_terminate_timeout = 600s
|
||||
|
||||
; PHP Admin Values (Segurança & Performance)
|
||||
php_admin_value[error_log] = /var/log/php8.3-fpm/zabbix-error.log
|
||||
php_admin_flag[log_errors] = on
|
||||
php_admin_value[memory_limit] = 512M
|
||||
php_value[session.save_handler] = redis
|
||||
php_value[session.save_path] = "tcp://127.0.0.1:6379?auth=SuaSenhaRedisForteAqui123!"
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
############ CONFIGURAÇÃO OTIMIZADA ZABBIX SERVER 7.0 LTS ############
|
||||
# Otimizado para: 4 núcleos CPU, 8GB RAM
|
||||
# Ambiente: ~1000 equipamentos diversos
|
||||
# Data: 2024
|
||||
####################################################################
|
||||
|
||||
###############################################################
|
||||
# OPÇÕES GERAIS
|
||||
###############################################################
|
||||
|
||||
# Arquivo de Log
|
||||
LogFile=/var/log/zabbix/zabbix_server.log
|
||||
LogFileSize=100
|
||||
DebugLevel=3
|
||||
|
||||
# Arquivo PID
|
||||
PidFile=/run/zabbix/zabbix_server.pid
|
||||
SocketDir=/run/zabbix
|
||||
|
||||
# Usuário do banco de dados (Ajuste a senha!)
|
||||
DBHost=localhost
|
||||
DBName=zabbix
|
||||
DBUser=zabbix
|
||||
DBPassword=sua_senha_aqui
|
||||
DBPort=5432
|
||||
|
||||
###############################################################
|
||||
# PERFORMANCE - PROCESSOS (POLLERS)
|
||||
###############################################################
|
||||
# Total de processos deve ser equilibrado com núcleos de CPU
|
||||
|
||||
# Pollers gerais (SNMP, Agente, etc)
|
||||
StartPollers=25
|
||||
|
||||
# Pollers para hosts indisponíveis (Reduz impacto de timeouts)
|
||||
StartPollersUnreachable=8
|
||||
|
||||
# Trappers (Importante para Zabbix Proxy e Agente Ativo)
|
||||
StartTrappers=15
|
||||
|
||||
# Pingers (ICMP)
|
||||
StartPingers=8
|
||||
|
||||
# Descoberta de Rede
|
||||
StartDiscoverers=5
|
||||
|
||||
# Monitoramento Web
|
||||
StartHTTPPollers=5
|
||||
|
||||
# Pre-processamento (Importante para regex, javascript, etc)
|
||||
StartPreprocessors=10
|
||||
|
||||
# LLD (Descoberta de baixo nível)
|
||||
StartLLDProcessors=6
|
||||
|
||||
# Conectores com banco de dados
|
||||
StartDBSyncers=8
|
||||
|
||||
###############################################################
|
||||
# CACHES - OTIMIZADO PARA 8GB RAM
|
||||
###############################################################
|
||||
# Zabbix Server não deve usar swap. Mantenha caches em RAM.
|
||||
|
||||
# Cache de configurações (Hosts, Items, Triggers)
|
||||
CacheSize=512M
|
||||
|
||||
# Cache de dados históricos (Buffer antes de gravar no banco)
|
||||
HistoryCacheSize=1G
|
||||
HistoryIndexCacheSize=256M
|
||||
|
||||
# Cache de dados de tendência
|
||||
TrendCacheSize=256M
|
||||
TrendFunctionCacheSize=64M
|
||||
|
||||
# Value Cache (Reduz queries ao banco para triggers)
|
||||
ValueCacheSize=768M
|
||||
|
||||
###############################################################
|
||||
# TIMEOUTS & INTERVALOS
|
||||
###############################################################
|
||||
# Timeouts um pouco maiores para evitar "nodata" em redes lentas
|
||||
|
||||
Timeout=15
|
||||
UnreachableTimeout=30
|
||||
TrapperTimeout=300
|
||||
UnreachablePeriod=45
|
||||
UnavailableDelay=60
|
||||
|
||||
# Frequência de escrita no banco
|
||||
CacheUpdateFrequency=60
|
||||
|
||||
###############################################################
|
||||
# CASA (HOUSEKEEPING) - Postgres + TimescaleDB
|
||||
###############################################################
|
||||
# Com TimescaleDB, o Zabbix não precisa deletar histórico linha por linha.
|
||||
# O Housekeeping do Zabbix foca em Events, Sessions, etc.
|
||||
|
||||
HousekeepingFrequency=1
|
||||
MaxHousekeeperDelete=5000
|
||||
|
||||
# Desabilitar housekeeping de histórico/trends pois TimescaleDB gerencia isso
|
||||
# (Se não usar TimescaleDB, comente essas linhas)
|
||||
# DisableHousekeeping=0 se não tiver TimescaleDB
|
||||
# Mas assumindo TimescaleDB configurado:
|
||||
|
||||
###############################################################
|
||||
# OTIMIZAÇÕES DE REDE
|
||||
###############################################################
|
||||
|
||||
ListenPort=10051
|
||||
SourceIP=0.0.0.0
|
||||
|
||||
###############################################################
|
||||
# MONITORAMENTO VMWARE
|
||||
###############################################################
|
||||
|
||||
StartVMwareCollectors=4
|
||||
VMwareFrequency=60
|
||||
VMwarePerfFrequency=60
|
||||
VMwareCacheSize=256M
|
||||
VMwareTimeout=30
|
||||
|
||||
###############################################################
|
||||
# MONITORAMENTO JAVA (JMX)
|
||||
###############################################################
|
||||
|
||||
# JavaGateway=127.0.0.1
|
||||
# JavaGatewayPort=10052
|
||||
# StartJavaPollers=5
|
||||
|
||||
###############################################################
|
||||
# ALERTAS & SCRIPTS
|
||||
###############################################################
|
||||
|
||||
StartAlerters=8
|
||||
AlertScriptsPath=/usr/lib/zabbix/alertscripts
|
||||
ExternalScripts=/usr/lib/zabbix/externalscripts
|
||||
|
||||
###############################################################
|
||||
# WEB SERVICE (RELATÓRIOS PDF)
|
||||
###############################################################
|
||||
|
||||
WebServiceURL=http://localhost:10053/report
|
||||
StartReportWriters=3
|
||||
|
||||
###############################################################
|
||||
# SECURITY / TLS
|
||||
###############################################################
|
||||
|
||||
# StatsAllowedIP=127.0.0.1
|
||||
|
||||
###############################################################
|
||||
# FIM
|
||||
###############################################################
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
import requests
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
# ==============================================================================
|
||||
# 🛠️ CONFIGURAÇÕES DO USUÁRIO - EDITE AQUI
|
||||
# ==============================================================================
|
||||
# URL do seu Zabbix antigo (ex: http://192.168.1.100/zabbix/api_jsonrpc.php)
|
||||
ZABBIX_URL = "http://172.16.254.11/zabbix/api_jsonrpc.php"
|
||||
|
||||
# Credenciais de um usuário com permissão de leitura total (Admin ou Super Admin)
|
||||
ZABBIX_USER = "Admin"
|
||||
ZABBIX_PASS = "M@dC@tMK11"
|
||||
|
||||
# Nome do arquivo de saída
|
||||
OUTPUT_FILE = "zabbix_hosts_export.json"
|
||||
|
||||
# ==============================================================================
|
||||
# 🚀 INÍCIO DO SCRIPT (Não precisa editar abaixo, a menos que saiba o que faz)
|
||||
# ==============================================================================
|
||||
|
||||
def print_header():
|
||||
print("="*60)
|
||||
print(" 📦 EXPORTADOR DE HOSTS ZABBIX - BASEADO NO PADRÃO ARTHUR")
|
||||
print("="*60)
|
||||
print(f"📅 Data: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}")
|
||||
print(f"🎯 Alvo: {ZABBIX_URL}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
def zabbix_api_call(method, params, auth_token=None, request_id=1):
|
||||
"""
|
||||
Faz uma chamada JSON-RPC para a API do Zabbix.
|
||||
"""
|
||||
headers = {'Content-Type': 'application/json-rpc'}
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": request_id
|
||||
}
|
||||
|
||||
if auth_token:
|
||||
# Padrão: Auth no corpo. O main() pode alterar isso se detectar 6.4+
|
||||
# Se o token começar com 'Bearer', usamos via header (adaptação dinâmica)
|
||||
if str(auth_token).startswith("USE_HEADER:"):
|
||||
real_token = auth_token.split(":", 1)[1]
|
||||
headers["Authorization"] = f"Bearer {real_token}"
|
||||
else:
|
||||
payload["auth"] = auth_token
|
||||
|
||||
try:
|
||||
# verify=False é crucial para Zabbix legados com certificados inválidos
|
||||
response = requests.post(ZABBIX_URL, data=json.dumps(payload), headers=headers, verify=False, timeout=30)
|
||||
response.raise_for_status()
|
||||
decoded = response.json()
|
||||
|
||||
if 'error' in decoded:
|
||||
# Se for erro de "Unexpected parameter 'auth'", tentamos retry sem auth no body?
|
||||
# Melhor garantir na detecção de versão.
|
||||
print(f"\n❌ ERRO NA API ZABBIX ({method}):")
|
||||
print(f" Mensagem: {decoded['error'].get('message', 'Sem mensagem')}")
|
||||
print(f" Detalhes: {decoded['error'].get('data', 'Sem detalhes')}")
|
||||
sys.exit(1)
|
||||
|
||||
return decoded['result']
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"\n❌ ERRO DE CONEXÃO:")
|
||||
print(f" Não foi possível conectar em: {ZABBIX_URL}")
|
||||
print(" Verifique se a URL está correta e se o servidor está acessível.")
|
||||
sys.exit(1)
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"\n❌ ERRO DE TIMEOUT:")
|
||||
print(" O servidor demorou muito para responder.")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n❌ ERRO INESPERADO: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
# Desabilita warnings chatos de SSL inseguro
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
print_header()
|
||||
|
||||
# 1. Autenticação
|
||||
print("🔐 1. Autenticando no Zabbix...", end=" ")
|
||||
try:
|
||||
# Tenta conectar usando 'username' (padrão 5.4+)
|
||||
try:
|
||||
auth_token = zabbix_api_call("user.login", {"username": ZABBIX_USER, "password": ZABBIX_PASS})
|
||||
except SystemExit:
|
||||
# Se falhar (ex: Zabbix antigo < 5.4), tenta com 'user'
|
||||
print("⚠️ Falha com 'username', tentando com método legado 'user'...")
|
||||
auth_token = zabbix_api_call("user.login", {"user": ZABBIX_USER, "password": ZABBIX_PASS})
|
||||
print(f"✅ Sucesso!\n 🔑 Token: {auth_token[:10]}...")
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"❌ Falha: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 2. Obter versão da API (opcional, mas bom pra debug)
|
||||
print("\n🔍 2. Verificando versão da API...", end=" ")
|
||||
api_info = zabbix_api_call("apiinfo.version", {}, auth_token=None)
|
||||
print(f"✅ Versão detectada: {api_info}")
|
||||
|
||||
# DECISÃO DE AUTH: Zabbix 6.4+ prefere Bearer Token no Header
|
||||
use_header_auth = False
|
||||
try:
|
||||
major_minor = float(api_info[:3]) # Pega "6.0", "7.0"
|
||||
if major_minor >= 6.4:
|
||||
print(f" ℹ️ Zabbix moderno (>6.4) detectado. Usando Auth via Header.")
|
||||
# Marcamos o token para ser usado via header
|
||||
auth_token = f"USE_HEADER:{auth_token}"
|
||||
use_header_auth = True
|
||||
except ValueError:
|
||||
pass # Versão estranha, mantém legado
|
||||
|
||||
# 3. Listar Hosts
|
||||
print("\n📋 3. Buscando lista de hosts...", end=" ")
|
||||
hosts = zabbix_api_call("host.get", {
|
||||
"output": ["hostid", "name"],
|
||||
"preservekeys": True
|
||||
}, auth_token)
|
||||
|
||||
host_ids = list(hosts.keys())
|
||||
count = len(host_ids)
|
||||
print(f"✅ Encontrados: {count} hosts.")
|
||||
|
||||
if count == 0:
|
||||
print("\n⚠️ Nenhum host encontrado para exportar. Encerrando.")
|
||||
return
|
||||
|
||||
# 4. Exportar (O Heavy Lifting)
|
||||
print(f"\n📦 4. Iniciando exportação completa (Configuration Export)...")
|
||||
print(" Isso pode demorar um pouco dependendo do tamanho da base. Aguarde...")
|
||||
|
||||
export_params = {
|
||||
"options": {
|
||||
"hosts": host_ids
|
||||
},
|
||||
"format": "json"
|
||||
}
|
||||
|
||||
try:
|
||||
export_data = zabbix_api_call("configuration.export", export_params, auth_token)
|
||||
except SystemExit:
|
||||
print("\n❌ Falha na exportação. Verifique se o usuário tem permissão para exportar hosts.")
|
||||
sys.exit(1)
|
||||
|
||||
# 5. Salvar Arquivo
|
||||
print(f"\n💾 5. Salvando arquivo '{OUTPUT_FILE}'...", end=" ")
|
||||
try:
|
||||
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
|
||||
if isinstance(export_data, str):
|
||||
f.write(export_data)
|
||||
else:
|
||||
json.dump(export_data, f, indent=4, ensure_ascii=False)
|
||||
print("✅ Sucesso!")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Erro ao salvar arquivo: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Conclusão
|
||||
print("\n" + "="*60)
|
||||
print("🎉 PROCESSO CONCLUÍDO COM SUCESSO!")
|
||||
print(f" Arquivo gerado: {OUTPUT_FILE}")
|
||||
print(" Total de hosts: ", count)
|
||||
print("="*60)
|
||||
print("\n👉 COMO IMPORTAR:")
|
||||
print(" 1. Vá no seu novo Zabbix > Configuration > Hosts")
|
||||
print(" 2. Clique em 'Import'")
|
||||
print(f" 3. Selecione o arquivo '{OUTPUT_FILE}'")
|
||||
print(" 4. Marque 'Create new' e 'Update existing'")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import requests
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# ==============================================================================
|
||||
# 🛠️ CONFIGURAÇÕES (JÁ PREENCHIDAS COM BASE NO ANTERIOR)
|
||||
# ==============================================================================
|
||||
ZABBIX_URL = "http://172.16.254.11/zabbix/api_jsonrpc.php"
|
||||
ZABBIX_USER = "Admin"
|
||||
ZABBIX_PASS = "M@dC@tMK11"
|
||||
OUTPUT_FILE = "zabbix_templates_export.json"
|
||||
|
||||
def zabbix_api_call(method, params, auth_token=None, request_id=1):
|
||||
headers = {'Content-Type': 'application/json-rpc'}
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": request_id
|
||||
}
|
||||
|
||||
if auth_token:
|
||||
# Lógica inteligente de Auth (Header vs Body)
|
||||
if str(auth_token).startswith("USE_HEADER:"):
|
||||
real_token = auth_token.split(":", 1)[1]
|
||||
headers["Authorization"] = f"Bearer {real_token}"
|
||||
else:
|
||||
payload["auth"] = auth_token
|
||||
|
||||
try:
|
||||
response = requests.post(ZABBIX_URL, data=json.dumps(payload), headers=headers, verify=False, timeout=60)
|
||||
response.raise_for_status()
|
||||
decoded = response.json()
|
||||
if 'error' in decoded:
|
||||
print(f"❌ Erro API ({method}): {decoded['error'].get('data')}")
|
||||
sys.exit(1)
|
||||
return decoded['result']
|
||||
except Exception as e:
|
||||
print(f"❌ Erro Conexão: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
print("="*60)
|
||||
print(" 📦 EXPORTADOR DE TEMPLATES ZABBIX")
|
||||
print("="*60)
|
||||
|
||||
# 1. Auth e Versão
|
||||
print("🔐 1. Autenticando...", end=" ")
|
||||
api_info = zabbix_api_call("apiinfo.version", {}, None)
|
||||
|
||||
# Tenta Auth Moderna (Username)
|
||||
try:
|
||||
auth_token = zabbix_api_call("user.login", {"username": ZABBIX_USER, "password": ZABBIX_PASS})
|
||||
except SystemExit:
|
||||
auth_token = zabbix_api_call("user.login", {"user": ZABBIX_USER, "password": ZABBIX_PASS})
|
||||
|
||||
# Adapta para Header se for Zabbix novo
|
||||
try:
|
||||
if float(api_info[:3]) >= 6.4:
|
||||
auth_token = f"USE_HEADER:{auth_token}"
|
||||
except: pass
|
||||
|
||||
print(f"✅ OK (API {api_info})")
|
||||
|
||||
# 2. Listar Templates
|
||||
print("\n📋 2. Buscando TODOS os templates...", end=" ")
|
||||
templates = zabbix_api_call("template.get", {
|
||||
"output": ["templateid", "name"],
|
||||
"preservekeys": True
|
||||
}, auth_token)
|
||||
|
||||
tpl_ids = list(templates.keys())
|
||||
count = len(tpl_ids)
|
||||
print(f"✅ {count} templates encontrados.")
|
||||
|
||||
if count == 0: return
|
||||
|
||||
# 3. Exportar (EM LOTES para evitar Error 500)
|
||||
print(f"\n📦 3. Baixando templates em lotes (evita timeout)...")
|
||||
|
||||
BATCH_SIZE = 50
|
||||
all_export_data = {"zabbix_export": {"version": "6.0", "templates": []}} # Estrutura base
|
||||
|
||||
# Nota: A fusão de JSONs de exportação é complexa.
|
||||
# Para simplificar e garantir que tenhamos os dados, vamos salvar VÁRIOS arquivos se der erro 500 no massivo,
|
||||
# OU tentar exportar um por um se o lote falhar.
|
||||
# Mas o configuration.export não suporta append fácil.
|
||||
|
||||
# TENTATIVA 1: Exportar tudo (já falhou com 500).
|
||||
# TENTATIVA 2: Exportar 1 a 1 e salvar numa pasta 'templates_bkp'?
|
||||
# Ou exportar em 2 ou 3 grandes blocos. Vamos tentar blocos menores.
|
||||
|
||||
import os
|
||||
if not os.path.exists("templates_export"):
|
||||
os.makedirs("templates_export")
|
||||
|
||||
total_exported = 0
|
||||
|
||||
for i in range(0, count, BATCH_SIZE):
|
||||
batch_ids = tpl_ids[i : i + BATCH_SIZE]
|
||||
print(f" ⏳ Processando lote {i+1} a {min(i+BATCH_SIZE, count)}...", end=" ")
|
||||
|
||||
try:
|
||||
batch_data = zabbix_api_call("configuration.export", {
|
||||
"options": {"templates": batch_ids},
|
||||
"format": "json"
|
||||
}, auth_token)
|
||||
|
||||
# Salva lote individualmente para garantir
|
||||
filename = f"templates_export/batch_{i//BATCH_SIZE + 1}.json"
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
if isinstance(batch_data, str): f.write(batch_data)
|
||||
else: json.dump(batch_data, f, indent=4, ensure_ascii=False)
|
||||
|
||||
print(f"✅ Salvo em {filename}")
|
||||
total_exported += len(batch_ids)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Falha no lote: {e}")
|
||||
|
||||
print(f"\n✅ Total exportado: {total_exported}/{count} templates.")
|
||||
print(f"📂 Os arquivos estão na pasta 'templates_export/'")
|
||||
print("ℹ️ Devido ao erro 500, dividimos em vários arquivos para garantir que você tenha tudo.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import yaml
|
||||
import uuid
|
||||
import re
|
||||
|
||||
file_path = "gold_edition/template_windows_os_gold.yaml"
|
||||
group_uuid_raw = "a571c0d144b14fd4a87a9d9b2aa9fcd6"
|
||||
|
||||
def is_valid_32_uuid(val):
|
||||
# exact 32 hex chars
|
||||
return bool(re.match(r'^[0-9a-f]{32}$', val.lower()))
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = yaml.safe_load(f)
|
||||
|
||||
# Update Group UUID
|
||||
for group in content.get('template_groups', []):
|
||||
if group['name'] == 'Templates/Applications':
|
||||
group['uuid'] = group_uuid_raw
|
||||
|
||||
# Fix other UUIDs
|
||||
uuid_map = {}
|
||||
|
||||
def fix(node):
|
||||
if isinstance(node, dict):
|
||||
if 'uuid' in node:
|
||||
val = str(node['uuid']).replace('-', '') # Strip dashes if present
|
||||
|
||||
# If it matches our target group UUID, keep it
|
||||
if val == group_uuid_raw:
|
||||
node['uuid'] = val
|
||||
elif not is_valid_32_uuid(val) or val.isdigit(): # regenerate if invalid OR if it looks like just numbers (failed manual fixes)
|
||||
if val not in uuid_map:
|
||||
uuid_map[val] = str(uuid.uuid4()).replace('-', '')
|
||||
node['uuid'] = uuid_map[val]
|
||||
else:
|
||||
node['uuid'] = val # use stripped version
|
||||
|
||||
for k, v in node.items():
|
||||
fix(v)
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
fix(item)
|
||||
|
||||
fix(content)
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(content, f, sort_keys=False, allow_unicode=True)
|
||||
|
||||
print("UUIDs fixed (32 chars).")
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import yaml
|
||||
import uuid
|
||||
import sys
|
||||
|
||||
def fix_uuids(file_path, target_group_uuid):
|
||||
print(f"Processing {file_path}...")
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# 1. Fix Group UUID
|
||||
if 'template_groups' in data:
|
||||
for group in data['template_groups']:
|
||||
if group['name'] == 'Templates/Applications':
|
||||
print(f"Updating Group UUID for {group['name']} to {target_group_uuid}")
|
||||
group['uuid'] = target_group_uuid
|
||||
|
||||
# 2. Regenerate all other UUIDs and clean tags
|
||||
count = 0
|
||||
generated_uuids = set()
|
||||
|
||||
def regenerate(node):
|
||||
nonlocal count
|
||||
if isinstance(node, dict):
|
||||
# Clean unsupported tags
|
||||
if 'wizard_ready' in node:
|
||||
del node['wizard_ready']
|
||||
if 'readme' in node:
|
||||
del node['readme']
|
||||
if 'config' in node: # Often found in macros in Zabbix 8.0
|
||||
del node['config']
|
||||
|
||||
if 'uuid' in node:
|
||||
# Skip if it's the group UUID we just fixed manually
|
||||
if node['uuid'] == target_group_uuid:
|
||||
pass
|
||||
else:
|
||||
new_uuid = uuid.uuid4().hex
|
||||
# Ensure uniqueness (paranoid check)
|
||||
while new_uuid in generated_uuids:
|
||||
new_uuid = uuid.uuid4().hex
|
||||
|
||||
node['uuid'] = new_uuid
|
||||
generated_uuids.add(new_uuid)
|
||||
count += 1
|
||||
|
||||
for k, v in list(node.items()): # Use list() to avoid runtime error during iteration if keys are deleted
|
||||
regenerate(v)
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
regenerate(item)
|
||||
|
||||
regenerate(data)
|
||||
print(f"Regenerated {count} UUIDs and cleaned tags.")
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(data, f, sort_keys=False, indent=2, width=float("inf"), allow_unicode=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Fix UUIDs in Zabbix Template")
|
||||
parser.add_argument("file", help="Path to the YAML template file")
|
||||
parser.add_argument("--group-uuid", default="a571c0d144b14fd4a87a9d9b2aa9fcd6", help="Target Group UUID")
|
||||
|
||||
args = parser.parse_args()
|
||||
fix_uuids(args.file, args.group_uuid)
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import sys
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
def get_group_uuid(url, token, group_name):
|
||||
api_url = url.rstrip('/') + "/api_jsonrpc.php"
|
||||
headers = {
|
||||
'Content-Type': 'application/json-rpc',
|
||||
'Authorization': f'Bearer {token}'
|
||||
}
|
||||
|
||||
# Looking for 'template_groups' (Zabbix 7.0+) or 'hostgroups'
|
||||
# Try template groups first
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "templategroup.get",
|
||||
"params": {
|
||||
"output": ["uuid", "name"],
|
||||
"filter": {
|
||||
"name": [group_name]
|
||||
}
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
|
||||
data = json.dumps(payload).encode('utf-8')
|
||||
req = urllib.request.Request(api_url, data=data, headers=headers, method='POST')
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
resp = json.loads(response.read().decode('utf-8'))
|
||||
if 'result' in resp and resp['result']:
|
||||
print(f"UUID: {resp['result'][0]['uuid']}")
|
||||
return
|
||||
else:
|
||||
# Try hostgroup.get as fallback if strict 6.0/7.0 compat varies
|
||||
payload['method'] = "hostgroup.get"
|
||||
data = json.dumps(payload).encode('utf-8')
|
||||
req = urllib.request.Request(api_url, data=data, headers=headers, method='POST')
|
||||
with urllib.request.urlopen(req) as response2:
|
||||
resp2 = json.loads(response2.read().decode('utf-8'))
|
||||
if 'result' in resp2 and resp2['result']:
|
||||
print(f"UUID: {resp2['result'][0]['uuid']}")
|
||||
else:
|
||||
print("NOT FOUND")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
get_group_uuid("https://noc.itguys.com.br/", "e59ff1a478bfb6c82a5654145c65498719b448c3f7f7af9e56d1e833c42d3fef", "Templates/Applications")
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
import yaml
|
||||
import uuid
|
||||
import sys
|
||||
|
||||
# Source files
|
||||
SOURCE_TEMPLATE = r"c:\Users\joao.goncalves\Downloads\custom_zabbix_templates\base_sources\template_exchange_source.yaml"
|
||||
GOLD_TEMPLATE = r"c:\Users\joao.goncalves\Downloads\custom_zabbix_templates\gold_edition\template_exchange_gold.yaml"
|
||||
TARGET_FILE = r"c:\Users\joao.goncalves\Downloads\custom_zabbix_templates\gold_edition\template_exchange_gold.yaml"
|
||||
|
||||
# Target Group UUID
|
||||
GROUP_UUID = "a571c0d144b14fd4a87a9d9b2aa9fcd6"
|
||||
|
||||
# Portuguese Translations Map (English substring -> Portuguese replacement)
|
||||
TRANSLATIONS = {
|
||||
"Microsoft Exchange Server 2016 by Zabbix agent": "Microsoft Exchange Gold Edition",
|
||||
"Databases total mounted": "Exchange: Bancos de Dados Montados (Total)",
|
||||
"ActiveSync: ping command pending": "ActiveSync: Comandos Ping Pendentes",
|
||||
"ActiveSync: requests per second": "ActiveSync: Requisições por segundo",
|
||||
"ActiveSync: sync commands per second": "ActiveSync: Comandos Sync por segundo",
|
||||
"Autodiscover: requests per second": "Autodiscover: Requisições por segundo",
|
||||
"Availability Service: availability requests per second": "Availability Service: Requisições por segundo",
|
||||
"Outlook Web App: current unique users": "OWA: Usuários Únicos Atuais",
|
||||
"Outlook Web App: requests per second": "OWA: Requisições por segundo",
|
||||
"MSExchangeWS: requests per second": "WebServices: Requisições por segundo",
|
||||
"Databases discovery": "Descoberta de Bancos de Dados",
|
||||
"Active Manager [{#INSTANCE}]: Database copy role": "Banco [{#INSTANCE}]: Função da Cópia",
|
||||
"Information Store [{#INSTANCE}]: Page faults per second": "Banco [{#INSTANCE}]: Page Faults/seg",
|
||||
"Information Store [{#INSTANCE}]: Log records stalled": "Banco [{#INSTANCE}]: Logs Travados (Stalled)",
|
||||
"Information Store [{#INSTANCE}]: Log threads waiting": "Banco [{#INSTANCE}]: Threads de Log Aguardando",
|
||||
"Active database read operations per second": "Leitura: Operações/seg (Ativo)",
|
||||
"Active database read operations latency": "Leitura: Latência Média (Ativo)",
|
||||
"Passive database read operations latency": "Leitura: Latência Média (Passivo)",
|
||||
"Active database write operations per second": "Escrita: Operações/seg (Ativo)",
|
||||
"Active database write operations latency": "Escrita: Latência Média (Ativo)",
|
||||
"Passive database write operations latency": "Escrita: Latência Média (Passivo)",
|
||||
"Information Store [{#INSTANCE}]: Active mailboxes count": "Banco [{#INSTANCE}]: Mailboxes Ativas",
|
||||
"Information Store [{#INSTANCE}]: Database state": "Banco [{#INSTANCE}]: Status",
|
||||
"Information Store [{#INSTANCE}]: RPC requests latency": "Banco [{#INSTANCE}]: Latência RPC Média",
|
||||
"Information Store [{#INSTANCE}]: RPC requests per second": "Banco [{#INSTANCE}]: Requisições RPC/seg",
|
||||
"Information Store [{#INSTANCE}]: RPC requests total": "Banco [{#INSTANCE}]: Total Requisições RPC",
|
||||
"LDAP discovery": "Descoberta de LDAP",
|
||||
"Domain Controller [{#INSTANCE}]: Read time": "DC [{#INSTANCE}]: Tempo de Leitura LDAP",
|
||||
"Domain Controller [{#INSTANCE}]: Search time": "DC [{#INSTANCE}]: Tempo de Busca LDAP",
|
||||
"Web services discovery": "Descoberta de Web Services",
|
||||
"Web Service [{#INSTANCE}]: Current connections": "Web Service [{#INSTANCE}]: Conexões Atuais",
|
||||
# Description Translations
|
||||
"Shows the number of active database copies on the server.": "Mostra o número de cópias ativas de banco de dados no servidor.",
|
||||
"Shows the number of ping commands currently pending in the queue.": "Mostra o número de comandos de ping pendentes na fila.",
|
||||
"Shows the number of HTTP requests received from the client via ASP.NET per second. Determines the current Exchange ActiveSync request rate. Used only to determine current user load.": "Mostra o número de requisições HTTP recebidas via ASP.NET/seg. Determina a carga atual de usuários ActiveSync.",
|
||||
"Shows the number of sync commands processed per second. Clients use this command to synchronize items within a folder.": "Mostra o número de comandos de sincronização processados/seg.",
|
||||
"Shows the number of Autodiscover service requests processed each second. Determines current user load.": "Mostra o número de requisições Autodiscover processadas/seg.",
|
||||
"Shows the number of requests serviced per second. The request can be only for free/ busy information or include suggestions. One request may contain multiple mailboxes. Determines the rate at which Availability service requests are occurring.": "Mostra o número de requisições de disponibilidade (Free/Busy) atendidas/seg.",
|
||||
"Shows the number of unique users currently logged on to Outlook Web App. This value monitors the number of unique active user sessions, so that users are only removed from this counter after they log off or their session times out. Determines current user load.": "Número de usuários únicos logados no OWA. Monitora sessões ativas (só reduz após logoff ou timeout).",
|
||||
"Shows the number of requests handled by Outlook Web App per second. Determines current user load.": "Número de requisições OWA processadas/seg.",
|
||||
"Shows the number of requests processed each second. Determines current user load.": "Número de requisições processadas/seg.",
|
||||
"Database copy active or passive role.": "Função da cópia do banco (Ativa ou Passiva).",
|
||||
"Indicates the rate of page faults that can't be serviced because there are no pages available for allocation from the database cache. If this counter is above 0, it's an indication that the MSExchange Database\\I/O Database Writes (Attached) Average Latency is too high.": "Taxa de falhas de página (Page Faults) não atendidas pelo cache. Se maior que 0, indica latência de disco alta.",
|
||||
"Too much page faults stalls for database \"{#INSTANCE}\". This counter should be 0 on production servers.": "Muitos Page Faults no banco \"{#INSTANCE}\". Deveria ser 0 em produção.",
|
||||
"Indicates the number of log records that can't be added to the log buffers per second because the log buffers are full. The average value should be below 10 per second. Spikes (maximum values) shouldn't be higher than 100 per second.": "Número de logs que não puderam ser gravados no buffer (Stalled). Média deve ser < 10/seg.",
|
||||
"Stalled log records too high. The average value should be less than 10 threads waiting.": "Muitos logs travados (stalled). A média deve ser menor que 10.",
|
||||
"Indicates the number of threads waiting to complete an update of the database by writing their data to the log.": "Número de threads aguardando para gravar no log do banco.",
|
||||
"Shows the number of database read operations.": "Número de operações de leitura no banco.",
|
||||
"Shows the average length of time per database read operation. Should be less than 20 ms on average.": "Tempo médio por operação de leitura. Deve ser menor que 20ms.",
|
||||
"Should be less than 20ms on average.": "Deve ser menor que 20ms em média.",
|
||||
"Shows the average length of time per passive database read operation. Should be less than 200ms on average.": "Tempo médio por leitura passiva. Deve ser menor que 200ms.",
|
||||
"Should be less than 200ms on average.": "Deve ser menor que 200ms em média.",
|
||||
"Shows the number of database write operations per second for each attached database instance.": "Número de operações de escrita/seg por instância.",
|
||||
"Shows the average length of time per database write operation. Should be less than 50ms on average.": "Tempo médio por operação de escrita. Deve ser menor que 50ms.",
|
||||
"Should be less than 50ms on average.": "Deve ser menor que 50ms em média.",
|
||||
"Shows the average length of time, in ms, per passive database write operation. Should be less than the read latency for the same instance, as measured by the MSExchange Database ==> Instances({#INF.STORE}/_Total)\\I/O Database Reads (Recovery) Average Latency counter.": "Tempo médio (ms) por escrita passiva.",
|
||||
"Should be less than the read latency for the same instance, as measured by the MSExchange Database ==> Instances({#INF.STORE}/_Total)\\I/O Database Reads (Recovery) Average Latency counter.": "Deve ser menor que a latência de leitura.",
|
||||
"Number of active mailboxes in this database.": "Número de mailboxes ativas neste banco.",
|
||||
"Database state. Possible values:\n0: Database without any copy and dismounted.\n1: Database is a primary database and mounted.\n2: Database is a passive copy and the state is healthy.": "Estado do banco (0=Desmontado, 1=Montado/Primário, 2=Saudável/Passivo).",
|
||||
"RPC Latency average is the average latency of RPC requests per database. Average is calculated over all RPCs since exrpc32 was loaded. Should be less than 50ms at all times, with spikes less than 100ms.": "Latência média de RPC por banco. Deve ser menor que 50ms.",
|
||||
"Should be less than 50ms at all times, with spikes less than 100ms.": "Deve ser menor que 50ms sempre.",
|
||||
"Shows the number of RPC operations per second for each database instance.": "Número de operações RPC/seg.",
|
||||
"Indicates the overall RPC requests currently executing within the information store process. Should be below 70 at all times.": "Total de requisições RPC em execução. Deve ser menor que 70.",
|
||||
"Should be below 70 at all times.": "Deve ser menor que 70 sempre.",
|
||||
"Time that it takes to send an LDAP read request to the domain controller in question and get a response. Should ideally be below 50 ms; spikes below 100 ms are acceptable.": "Tempo para enviar/receber leitura LDAP do DC. Ideal < 50ms.",
|
||||
"Time that it takes to send an LDAP search request and get a response. Should ideally be below 50 ms; spikes below 100 ms are acceptable.": "Tempo para busca LDAP. Ideal < 50ms.",
|
||||
"Shows the current number of connections established to the each Web Service.": "Número atual de conexões para cada Web Service.",
|
||||
"The template to monitor Microsoft Exchange Server 2016 by Zabbix that works without any external scripts.": "Template Gold para monitoramento do Exchange.",
|
||||
"The metrics are collected by Zabbix agent.": "Métricas coletadas pelo Zabbix Agent.",
|
||||
"Recommended to use it with \"OS Windows by Zabbix agent\" template.": "Recomendado usar junto com o template de SO Windows."
|
||||
}
|
||||
|
||||
def load_yaml(path):
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def clean_tags_and_fix_uuids(data):
|
||||
generated_uuids = set()
|
||||
|
||||
def process_node(node):
|
||||
if isinstance(node, dict):
|
||||
# Clean tags
|
||||
for tag in ['wizard_ready', 'readme', 'vendor', 'config']:
|
||||
if tag in node:
|
||||
del node[tag]
|
||||
|
||||
# Fix UUIDs
|
||||
if 'uuid' in node:
|
||||
# Keep group UUID if matches target
|
||||
if node['uuid'] == GROUP_UUID:
|
||||
pass
|
||||
else:
|
||||
new_uuid = uuid.uuid4().hex
|
||||
while new_uuid in generated_uuids:
|
||||
new_uuid = uuid.uuid4().hex
|
||||
node['uuid'] = new_uuid
|
||||
generated_uuids.add(new_uuid)
|
||||
|
||||
# Translate Strings
|
||||
for key in ['name', 'description', 'event_name', 'comment']:
|
||||
if key in node and isinstance(node[key], str):
|
||||
for eng, pt in TRANSLATIONS.items():
|
||||
node[key] = node[key].replace(eng, pt)
|
||||
|
||||
for k, v in list(node.items()):
|
||||
process_node(v)
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
process_node(item)
|
||||
|
||||
process_node(data)
|
||||
|
||||
def main():
|
||||
print("Loading Source Template...")
|
||||
source = load_yaml(SOURCE_TEMPLATE)
|
||||
|
||||
# 1. Update Header
|
||||
source['zabbix_export']['version'] = '7.0'
|
||||
template = source['zabbix_export']['templates'][0]
|
||||
template['name'] = "Microsoft Exchange Gold Edition"
|
||||
template['description'] = "Template Gold Edition para Microsoft Exchange (2016/2019).\n\nFuncionalidades:\n- Monitoramento Completo de Bancos de Dados (I/O, Latência, RPC)\n- Filas de Transporte e Back Pressure (Anti-Spam)\n- Serviços Críticos\n- Backup Age (PowerShell)\n- Acesso Web (OWA/ActiveSync)"
|
||||
|
||||
# 2. Update Group
|
||||
source['zabbix_export']['template_groups'][0]['uuid'] = GROUP_UUID
|
||||
template['groups'][0]['name'] = "Templates/Applications"
|
||||
|
||||
# 3. Add Custom "Gold" Items (Backup & New Metrics)
|
||||
|
||||
# Backup Item (from Old Gold)
|
||||
backup_item = {
|
||||
'uuid': '', # Will be generated
|
||||
'name': 'Exchange: Horas desde o último Backup Full',
|
||||
'key': 'system.run[powershell -NoProfile -Command "((Get-Date) - (Get-MailboxDatabase -Status | Sort-Object LastFullBackup | Select-Object -First 1 -ExpandProperty LastFullBackup)).TotalHours"]',
|
||||
'delay': '4h',
|
||||
'value_type': 'FLOAT',
|
||||
'units': 'h',
|
||||
'tags': [{'tag': 'component', 'value': 'backup'}],
|
||||
'triggers': [{
|
||||
'uuid': '',
|
||||
'expression': 'last(/Microsoft Exchange Gold Edition/system.run[powershell -NoProfile -Command "((Get-Date) - (Get-MailboxDatabase -Status | Sort-Object LastFullBackup | Select-Object -First 1 -ExpandProperty LastFullBackup)).TotalHours"])>30',
|
||||
'name': '🚨 Exchange: Backup Atrasado (> 30 Horas)',
|
||||
'priority': 'HIGH',
|
||||
'description': 'O backup Full não roda há mais de 30 horas. Risco de Log Transactional encher o disco.'
|
||||
}]
|
||||
}
|
||||
template['items'].append(backup_item)
|
||||
|
||||
# Back Pressure: Private Bytes
|
||||
private_bytes_item = {
|
||||
'uuid': '',
|
||||
'name': 'EdgeTransport: Consumo de RAM (Private Bytes)',
|
||||
'key': 'perf_counter_en["\\Process(EdgeTransport)\\Private Bytes"]',
|
||||
'delay': '1m',
|
||||
'value_type': 'FLOAT',
|
||||
'units': 'B',
|
||||
'tags': [{'tag': 'component', 'value': 'back_pressure'}],
|
||||
'triggers': [{
|
||||
'uuid': '',
|
||||
'expression': 'min(/Microsoft Exchange Gold Edition/perf_counter_en["\\Process(EdgeTransport)\\Private Bytes"],15m)>{$EXCHANGE.EDGE.MEM.MAX}',
|
||||
'name': '🚨 Exchange: EdgeTransport consumindo muita RAM (Possível Back Pressure)',
|
||||
'priority': 'AVERAGE',
|
||||
'description': 'O processo EdgeTransport está consumindo muita memória. Isso pode ativar o Back Pressure e rejeitar e-mails.'
|
||||
}]
|
||||
}
|
||||
template['items'].append(private_bytes_item)
|
||||
|
||||
# Back Pressure / Spam: Submission Queue
|
||||
submission_queue_item = {
|
||||
'uuid': '',
|
||||
'name': 'Exchange: Fila de Submissão (Submission Queue)',
|
||||
'key': 'perf_counter_en["\\MSExchangeTransport Queues(_Total)\\Submission Queue Length"]',
|
||||
'delay': '1m',
|
||||
'value_type': 'FLOAT',
|
||||
'tags': [{'tag': 'component', 'value': 'spam_protection'}],
|
||||
'triggers': [{
|
||||
'uuid': '',
|
||||
'expression': 'min(/Microsoft Exchange Gold Edition/perf_counter_en["\\MSExchangeTransport Queues(_Total)\\Submission Queue Length"],10m)>500',
|
||||
'name': '🚨 Exchange: Fila de Submissão Crítica (>500) - Possível SPAM',
|
||||
'priority': 'HIGH',
|
||||
'description': 'A fila de submissão está alta. Pode indicar um ataque de SPAM massivo entrando no servidor.'
|
||||
}]
|
||||
}
|
||||
template['items'].append(submission_queue_item)
|
||||
|
||||
# Database Size (Inject into Discovery Rule)
|
||||
# Finding "Databases discovery" rule
|
||||
db_discovery = next((d for d in template['discovery_rules'] if "Databases discovery" in d['name'] or "Descoberta de Bancos de Dados" in d['name']), None)
|
||||
if db_discovery:
|
||||
db_size_item = {
|
||||
'uuid': '',
|
||||
'name': 'Banco [{#INSTANCE}]: Tamanho do Arquivo (Bytes)',
|
||||
'key': 'system.run[powershell -NoProfile -Command "(Get-MailboxDatabase -Identity \'{#INSTANCE}\' -Status).DatabaseSize.ToBytes()"]',
|
||||
'delay': '1h',
|
||||
'value_type': 'FLOAT',
|
||||
'units': 'B',
|
||||
'description': 'Tamanho físico do arquivo do banco de dados (EDB).',
|
||||
'tags': [{'tag': 'component', 'value': 'database'}, {'tag': 'database', 'value': '{#INSTANCE}'}]
|
||||
}
|
||||
db_discovery['item_prototypes'].append(db_size_item)
|
||||
else:
|
||||
print("WARNING: Could not find Database Discovery rule to inject DB Size item.")
|
||||
|
||||
# Macros for new items
|
||||
template['macros'].append({'macro': '{$EXCHANGE.EDGE.MEM.MAX}', 'value': '20G', 'description': 'Máximo de RAM para EdgeTransport'})
|
||||
|
||||
# 4. Clean and Fix
|
||||
print("Cleaning tags, translating and regenerating UUIDs...")
|
||||
clean_tags_and_fix_uuids(source)
|
||||
|
||||
# 5. Save
|
||||
print(f"Saving to {TARGET_FILE}...")
|
||||
with open(TARGET_FILE, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(source, f, sort_keys=False, indent=2, width=float("inf"), allow_unicode=True)
|
||||
|
||||
print("Done!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
import yaml
|
||||
import uuid
|
||||
import sys
|
||||
|
||||
source_file = "gold_edition/template_windows_os_gold.yaml"
|
||||
target_file = "gold_edition/template_windows_platinum.yaml"
|
||||
|
||||
def regen_uuids(node):
|
||||
if isinstance(node, dict):
|
||||
if 'uuid' in node:
|
||||
node['uuid'] = str(uuid.uuid4()).replace('-', '')
|
||||
|
||||
# Rename template
|
||||
if 'template' in node and node['template'] == 'Windows Server - Gold Edition':
|
||||
node['template'] = 'Windows Server - Platinum Edition'
|
||||
if 'name' in node and node['name'] == 'Windows Server - Gold Edition':
|
||||
node['name'] = 'Windows Server - Platinum Edition'
|
||||
|
||||
for k, v in node.items():
|
||||
regen_uuids(v)
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
regen_uuids(item)
|
||||
|
||||
try:
|
||||
with open(source_file, 'r', encoding='utf-8') as f:
|
||||
content = yaml.safe_load(f)
|
||||
|
||||
regen_uuids(content)
|
||||
|
||||
with open(target_file, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(content, f, sort_keys=False, allow_unicode=True) # default_flow_style=False?
|
||||
|
||||
print("Successfully regenerated ALL UUIDs and renamed to Platinum.")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
import uuid
|
||||
import sys
|
||||
|
||||
target_file = "gold_edition/template_windows_os_gold.yaml"
|
||||
uuids_to_replace = [
|
||||
"d37d53fdc76c42988001e33bf7e214e6", "d37d53fdc76c42988001e33bf7e214e7",
|
||||
"58373569dba14f1f80da26504dfa066d", "58373569dba14f1f80da26504dfa066e",
|
||||
"9d939f611d57494bbf39f52a9f9e0b90", "9d939f611d57494bbf39f52a9f9e0b91",
|
||||
"8ebb2fd926ad4586b82be80c678e12d7", "8ebb2fd926ad4586b82be80c678e12d8",
|
||||
"91c24a78b4f4441cb4363387dc484900", "91c24a78b4f4441cb4363387dc484901",
|
||||
"5291dca6834f47e8aecc4ee75eaec725", "5291dca6834f47e8aecc4ee75eaec726",
|
||||
"a67bdcd7441d4d8fbd061fb6101fb378",
|
||||
"eaabeea88dee40b894ae1945956e2b55",
|
||||
"2df1c89eab4142f7a587b135930837c3", "2df1c89eab4142f7a587b135930837c2"
|
||||
]
|
||||
|
||||
with open(target_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
for u in uuids_to_replace:
|
||||
new_uuid = str(uuid.uuid4()).replace('-', '')
|
||||
if u in content:
|
||||
print(f"Replacing {u} -> {new_uuid}")
|
||||
content = content.replace(u, new_uuid)
|
||||
|
||||
with open(target_file, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print("Done.")
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
# Documentação: PFSense by SNMP
|
||||
|
||||
**Template:** PFSense by SNMP
|
||||
**Descrição:**
|
||||
Template para monitoramento do pfSense via SNMP
|
||||
Configuração:
|
||||
1. Habilite o daemon SNMP em Services na interface web do pfSense: https://docs.netgate.com/pfsense/en/latest/services/snmp.html
|
||||
2. Configure a regra de firewall para permitir acesso do Zabbix Proxy/Server via SNMP: https://docs.netgate.com/pfsense/en/latest/firewall/index.html#managing-firewall-rules
|
||||
3. Associe o template ao host.
|
||||
|
||||
MIBs usadas:
|
||||
BEGEMOT-PF-MIB
|
||||
HOST-RESOURCES-MIB
|
||||
|
||||
Gerado originalmente pela ferramenta oficial "Templator", Otimizado para Padrão Gold (Arthur)
|
||||
|
||||
|
||||
## Itens Monitorados
|
||||
|
||||
### Itens Globais
|
||||
- **Coleta Raw (SNMP): Interfaces de Rede PF** (`net.if.pf.walk`)
|
||||
- **Coleta Raw (SNMP): Interfaces de Rede (IF-MIB)** (`net.if.walk`)
|
||||
- **Status do servidor DHCP** (`pfsense.dhcpd.status`)
|
||||
- **Status do servidor DNS** (`pfsense.dns.status`)
|
||||
- **Estado do processo Nginx (Web)** (`pfsense.nginx.status`)
|
||||
- **Pacotes com offset incorreto (Bad Offset)** (`pfsense.packets.bad.offset`)
|
||||
- **Pacotes Fragmentados** (`pfsense.packets.fragment`)
|
||||
- **Pacotes correspondentes a uma regra de filtro** (`pfsense.packets.match`)
|
||||
- **Pacotes descartados por limite de memória** (`pfsense.packets.mem.drop`)
|
||||
- **Pacotes Normalizados** (`pfsense.packets.normalize`)
|
||||
- **Pacotes Curtos (Short Packets)** (`pfsense.packets.short`)
|
||||
- **Status de execução do Packet Filter** (`pfsense.pf.status`)
|
||||
- **Coleta Raw (SNMP): Contadores PF (pfCounter)** (`pfsense.pf_counters.walk`)
|
||||
- **Contagem de regras de Firewall** (`pfsense.rules.count`)
|
||||
- **Tabela de Rastreamento: Origens Atuais (Source Tracking)** (`pfsense.source.tracking.table.count`)
|
||||
- **Tabela de Rastreamento: Limite (Limit)** (`pfsense.source.tracking.table.limit`)
|
||||
- **Tabela de Rastreamento: Utilização (%)** (`pfsense.source.tracking.table.pused`)
|
||||
- **Tabela de Estados: Atual (State Table)** (`pfsense.state.table.count`)
|
||||
- **Tabela de Estados: Limite (Limit)** (`pfsense.state.table.limit`)
|
||||
- **Tabela de Estados: Utilização (%)** (`pfsense.state.table.pused`)
|
||||
- **Coleta Raw (SNMP): Software Instalado (hrSWRun)** (`pfsense.sw.walk`)
|
||||
- **Disponibilidade do Agente SNMP** (`zabbix[host,snmp,available]`)
|
||||
|
||||
### Regras de Descoberta (LLD)
|
||||
|
||||
#### Descoberta de interfaces de rede (`pfsense.net.if.discovery`)
|
||||
- **Protótipos de Itens:**
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de entrada bloqueado (`net.if.in.block.v4.bps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de entrada bloqueados (`net.if.in.block.v4.pps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de entrada bloqueado (`net.if.in.block.v6.bps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv6 de entrada bloqueados (`net.if.in.block.v6.pps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes de entrada descartados (`net.if.in.discards[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes de entrada com erros (`net.if.in.errors[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de entrada permitido (`net.if.in.pass.v4.bps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de entrada permitidos (`net.if.in.pass.v4.pps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de entrada permitido (`net.if.in.pass.v6.bps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv6 de entrada permitidos (`net.if.in.pass.v6.pps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Bits recebidos (`net.if.in[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de saída bloqueado (`net.if.out.block.v4.bps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de saída bloqueados (`net.if.out.block.v4.pps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de saída bloqueado (`net.if.out.block.v6.bps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes de saída descartados (`net.if.out.discards[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes de saída com erros (`net.if.out.errors[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de saída permitido (`net.if.out.pass.v4.bps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de saída permitidos (`net.if.out.pass.v4.pps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de saída permitido (`net.if.out.pass.v6.bps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv6 de saída permitidos (`net.if.out.pass.v6.pps[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Bits enviados (`net.if.out[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Contagem de referências de regras (`net.if.rules.refs[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Velocidade (`net.if.speed[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Status operacional (`net.if.status[{#SNMPINDEX}]`)
|
||||
- Interface [{#IFNAME}({#IFALIAS})]: Tipo de interface (`net.if.type[{#SNMPINDEX}]`)
|
||||
|
||||
## Alertas (Triggers)
|
||||
|
||||
### Triggers Globais
|
||||
- [AVERAGE] **🚨 DHCP Parado: Servidor DHCP não está em execução**
|
||||
- [AVERAGE] **🚨 DNS Parado: Servidor DNS (Unbound) não está em execução**
|
||||
- [AVERAGE] **🚨 WebServer Parado: Nginx não está rodando**
|
||||
- [HIGH] **🚨 Firewall Desligado: Packet Filter inativo**
|
||||
- [WARNING] **⚠️ Tabela de Rastreamento Cheia: Uso elevado ({ITEM.LASTVALUE1}%)**
|
||||
- [WARNING] **🔥 Tabela de Estados Crítica: Risco de bloqueio ({ITEM.LASTVALUE1}%)**
|
||||
- [WARNING] **🚨 Falha SNMP: Sem coleta de dados no pfSense**
|
||||
|
||||
### Protótipos de Triggers (LLD)
|
||||
|
||||
**Regra: Descoberta de interfaces de rede**
|
||||
- [INFO] **ℹ️ PFSense: Interface [{#IFNAME}({#IFALIAS})]: Velocidade da Ethernet diminuiu**
|
||||
- [WARNING] **⚠️ PFSense: Interface [{#IFNAME}({#IFALIAS})]: Alto uso de banda de entrada**
|
||||
- [WARNING] **⚠️ PFSense: Interface [{#IFNAME}({#IFALIAS})]: Alto uso de banda de saída**
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# 📜 Especificação Técnica: Template pfSense SNMP (Zabbix 7.0 Native)
|
||||
|
||||
Este documento define a especificação exata para a implementação das melhorias no template `template_app_pfsense_snmp.yaml`. O agente executor deve seguir estritamente as definições de UUID, Tags e Expressões abaixo.
|
||||
|
||||
## 1. Diretrizes de Engenharia (Reference: Zabbix 7.0 Advanced YAML)
|
||||
* **UUIDs:** Devem ser gerados novos para cada entidade (Item, Trigger, Dashboard). Formato: 32 chars hex lowercase (ex: `uuidgen | tr -d '-' | tr 'A-F' 'a-f'`).
|
||||
* **Tags:** Taxonomia obrigatória para todos os novos triggers.
|
||||
* `scope`: `availability` | `capacity` | `performance` | `security`
|
||||
* `layer`: `hardware` | `network` | `application`
|
||||
* **Macros:** Usar macros de template `{$MACRO}` para todos os thresholds.
|
||||
|
||||
## 2. Novos Triggers (Física & Segurança)
|
||||
|
||||
### 2.1 Interface Health (Contexto LLD: `net.if.discovery`)
|
||||
Aplicar nos **Trigger Prototypes**.
|
||||
|
||||
| Nome do Trigger | Expressão (Notação Zabbix) | Severidade | Tags |
|
||||
|---|---|---|---|
|
||||
| **🚨 Falha Física: Erros de Entrada Excessivos na {#IFNAME}** | `min(/PFSense by SNMP/net.if.in.errors[{#SNMPINDEX}],#3) > {$IF.ERRORS.WARN}` | HIGH | `scope:availability`, `layer:hardware` |
|
||||
| **⚠️ Possível Duplex Mismatch: Erros de Saída na {#IFNAME}** | `min(/PFSense by SNMP/net.if.out.errors[{#SNMPINDEX}],#3) > {$IF.ERRORS.WARN}` | HIGHER | `scope:performance`, `layer:network` |
|
||||
| **🐢 Congestionamento: Descartes na interface {#IFNAME}** | `min(/PFSense by SNMP/net.if.in.discards[{#SNMPINDEX}],5m) > {$IF.DISCARDS.WARN}` | WARNING | `scope:capacity`, `layer:network` |
|
||||
|
||||
### 2.2 Security & Integrity (Global)
|
||||
Aplicar em **Triggers Globais**.
|
||||
|
||||
| Nome do Trigger | Expressão | Sev | Tags |
|
||||
|---|---|---|---|
|
||||
| **🛡️ Possível Portscan: Pico de Bloqueios na WAN** | `min(/PFSense by SNMP/pfsense.packets.match, 5m) > avg(/PFSense by SNMP/pfsense.packets.match, 1h) * 3` | WARNING | `scope:security`, `type:anomaly` |
|
||||
| **🧩 Fragmentação Excessiva IPv4** | `min(/PFSense by SNMP/pfsense.packets.fragment, 5m) > {$PF.FRAG.MAX}` | WARNING | `scope:security` |
|
||||
|
||||
## 3. Análise Preditiva & Derivada (Calculated Items & Triggers)
|
||||
|
||||
### 3.1 Item Calculado: Noise Ratio
|
||||
Criar novo item calculado para medir a "sujeira" do tráfego.
|
||||
* **Key:** `pfsense.firewall.noise_ratio`
|
||||
* **Formula:** `last(//pfsense.packets.match) / ( last(//pfsense.packets.match) + last(//net.if.pass.total) + 0.001 )`
|
||||
* *Nota: `net.if.pass.total` deve ser um item de soma de todas as interfaces (pode ser complexo em SNMP puro, alternativa: usar apenas counters globais do PF se disponíveis).*
|
||||
* *Correção:* O PF MIB tem counters globais. Usaremos: `last(//pfsense.packets.match) / ( last(//pfsense.pf_counters.walk[...pass]) + ... )`. Simplificação para o agente: Usar apenas **Taxa de Bloqueio** se o total não estiver claro.
|
||||
* **Fallback Specification:** Usar Trigger em `pfsense.packets.match` (Bloqueio) comparado com baseline.
|
||||
|
||||
### 3.2 Triggers Preditivas (Forecast)
|
||||
|
||||
| Nome do Trigger | Expressão | Sev |
|
||||
|---|---|---|
|
||||
| **⏳ Tabela de Estados cheia em < 1h** | `timeleft(/PFSense by SNMP/pfsense.state.table.pused, 1h, 100) < 1h` | HIGH |
|
||||
| **⏳ Source Tracking cheia em < 1h** | `timeleft(/PFSense by SNMP/pfsense.source.tracking.table.pused, 1h, 100) < 1h` | HIGH |
|
||||
|
||||
## 4. Dashboards (Visualização como Código)
|
||||
Adicionar bloco `dashboards` no YAML root.
|
||||
|
||||
* **Dashboard:** "PFsense: Security & Health"
|
||||
* **Widgets:**
|
||||
1. **Graph (Stacked):** "Firewall Traffic Mix" -> Series: `pfsense.packets.match` (Red), `pfsense.packets.normalize` (Yellow).
|
||||
2. **Gauge:** "State Table Capacity" -> Item: `pfsense.state.table.pused`.
|
||||
3. **Top Hosts (Honeycomb style equivalent if 7.0):** "Interface Errors" -> LLD Items `net.if.in.errors[*]`.
|
||||
|
||||
## 5. Instruções de Execução
|
||||
1. **Edição:** Inserir os novos blocos no `template_app_pfsense_snmp.yaml`.
|
||||
2. **Validação:** Garantir que UUIDs sejam únicos (não reutilizar strings do exemplo).
|
||||
3. **Macros:** Adicionar as macros `{$IF.ERRORS.WARN}`, `{$IF.DISCARDS.WARN}`, `{$PF.FRAG.MAX}` no bloco `macros` do template.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,493 @@
|
|||
import yaml
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import argparse
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
# Fix for Windows console UTF-8 output (emojis)
|
||||
if sys.stdout.encoding != 'utf-8':
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
except AttributeError:
|
||||
pass # Python < 3.7 fallback
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION FUNCTIONS - Arthur Gold Edition
|
||||
# ============================================================================
|
||||
|
||||
def is_valid_uuidv4(uuid_str):
|
||||
"""
|
||||
Validate if a string is a proper UUIDv4 format.
|
||||
UUIDv4 rules (32 hex chars, no dashes):
|
||||
- Position 13 (0-indexed 12) must be '4' (version)
|
||||
- Position 17 (0-indexed 16) must be '8', '9', 'a', or 'b' (variant)
|
||||
"""
|
||||
if not isinstance(uuid_str, str):
|
||||
return False, "UUID is not a string"
|
||||
|
||||
# Remove dashes if present and lowercase
|
||||
clean = uuid_str.replace('-', '').lower()
|
||||
|
||||
if len(clean) != 32:
|
||||
return False, f"UUID has {len(clean)} chars (expected 32)"
|
||||
|
||||
if not re.match(r'^[0-9a-f]{32}$', clean):
|
||||
return False, "UUID contains non-hex characters"
|
||||
|
||||
# Check version (position 12, 0-indexed)
|
||||
if clean[12] != '4':
|
||||
return False, f"UUID version is '{clean[12]}' (expected '4' at position 13)"
|
||||
|
||||
# Check variant (position 16, 0-indexed)
|
||||
if clean[16] not in '89ab':
|
||||
return False, f"UUID variant is '{clean[16]}' (expected '8/9/a/b' at position 17)"
|
||||
|
||||
return True, "Valid UUIDv4"
|
||||
|
||||
|
||||
def validate_uuids_format(content):
|
||||
"""
|
||||
Recursively check all UUIDs in the template for valid UUIDv4 format.
|
||||
Returns list of errors.
|
||||
"""
|
||||
errors = []
|
||||
|
||||
def check_node(node, path="root"):
|
||||
if isinstance(node, dict):
|
||||
if 'uuid' in node:
|
||||
uuid = node['uuid']
|
||||
is_valid, msg = is_valid_uuidv4(uuid)
|
||||
if not is_valid:
|
||||
errors.append(f"[INVALID UUID] {uuid} at {path}: {msg}")
|
||||
|
||||
for k, v in node.items():
|
||||
check_node(v, f"{path}.{k}")
|
||||
elif isinstance(node, list):
|
||||
for i, item in enumerate(node):
|
||||
check_node(item, f"{path}[{i}]")
|
||||
|
||||
check_node(content)
|
||||
return errors
|
||||
|
||||
|
||||
def collect_item_keys(content):
|
||||
"""
|
||||
Collect all item and item_prototype keys from the template.
|
||||
Used for validating graph references.
|
||||
"""
|
||||
keys = set()
|
||||
|
||||
def extract(node, path="root"):
|
||||
if isinstance(node, dict):
|
||||
# Collect from items and item_prototypes
|
||||
if 'key' in node and ('type' in node or 'delay' in node or 'value_type' in node):
|
||||
keys.add(node['key'])
|
||||
|
||||
for k, v in node.items():
|
||||
extract(v, f"{path}.{k}")
|
||||
elif isinstance(node, list):
|
||||
for i, item in enumerate(node):
|
||||
extract(item, f"{path}[{i}]")
|
||||
|
||||
extract(content)
|
||||
return keys
|
||||
|
||||
|
||||
def collect_graph_names(content):
|
||||
"""
|
||||
Collect all graph and graph_prototype names from the template.
|
||||
Used for validating dashboard references.
|
||||
"""
|
||||
names = set()
|
||||
|
||||
def extract(node, path="root", in_graphs=False):
|
||||
if isinstance(node, dict):
|
||||
# Check if we're in a graphs section
|
||||
if 'name' in node and (in_graphs or 'graph_items' in node):
|
||||
names.add(node['name'])
|
||||
|
||||
for k, v in node.items():
|
||||
is_graph_section = k in ('graphs', 'graph_prototypes')
|
||||
extract(v, f"{path}.{k}", in_graphs or is_graph_section)
|
||||
elif isinstance(node, list):
|
||||
for i, item in enumerate(node):
|
||||
extract(item, f"{path}[{i}]", in_graphs)
|
||||
|
||||
extract(content)
|
||||
return names
|
||||
|
||||
|
||||
def validate_graph_references(content, item_keys):
|
||||
"""
|
||||
Check if all items referenced in graphs actually exist.
|
||||
Returns list of errors.
|
||||
"""
|
||||
errors = []
|
||||
|
||||
def check_graphs(node, path="root"):
|
||||
if isinstance(node, dict):
|
||||
# Check graph_items for item references
|
||||
if 'graph_items' in node:
|
||||
graph_name = node.get('name', 'Unknown')
|
||||
for i, graph_item in enumerate(node['graph_items']):
|
||||
if 'item' in graph_item and 'key' in graph_item['item']:
|
||||
ref_key = graph_item['item']['key']
|
||||
# Handle LLD macros - extract base pattern
|
||||
base_key = re.sub(r'\[.*\]', '[*]', ref_key)
|
||||
|
||||
# Check if key exists (exact match or pattern match)
|
||||
found = False
|
||||
for existing_key in item_keys:
|
||||
existing_base = re.sub(r'\[.*\]', '[*]', existing_key)
|
||||
if existing_base == base_key or existing_key == ref_key:
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
errors.append(f"[MISSING ITEM REF] Graph '{graph_name}' references non-existent item '{ref_key}'")
|
||||
|
||||
for k, v in node.items():
|
||||
check_graphs(v, f"{path}.{k}")
|
||||
elif isinstance(node, list):
|
||||
for i, item in enumerate(node):
|
||||
check_graphs(item, f"{path}[{i}]")
|
||||
|
||||
check_graphs(content)
|
||||
return errors
|
||||
|
||||
|
||||
def validate_dashboard_references(content, graph_names):
|
||||
"""
|
||||
Check if all graphs referenced in dashboards actually exist.
|
||||
Returns list of errors.
|
||||
"""
|
||||
errors = []
|
||||
|
||||
def check_dashboards(node, path="root"):
|
||||
if isinstance(node, dict):
|
||||
# Check for dashboard widget graph references
|
||||
if 'fields' in node and isinstance(node['fields'], list):
|
||||
widget_name = node.get('name', 'Unknown widget')
|
||||
for field in node['fields']:
|
||||
if isinstance(field, dict):
|
||||
if field.get('name') == 'graphid.0' and 'value' in field:
|
||||
value = field['value']
|
||||
if isinstance(value, dict) and 'name' in value:
|
||||
ref_name = value['name']
|
||||
if ref_name not in graph_names:
|
||||
errors.append(f"[MISSING GRAPH REF] Dashboard widget references non-existent graph '{ref_name}'")
|
||||
|
||||
for k, v in node.items():
|
||||
check_dashboards(v, f"{path}.{k}")
|
||||
elif isinstance(node, list):
|
||||
for i, item in enumerate(node):
|
||||
check_dashboards(item, f"{path}[{i}]")
|
||||
|
||||
check_dashboards(content)
|
||||
return errors
|
||||
|
||||
|
||||
def validate_yaml(file_path):
|
||||
print(f"Validating {file_path}...")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
print(f"[FATAL] Invalid YAML syntax: {e}")
|
||||
return False
|
||||
|
||||
if not content:
|
||||
print("[FATAL] Empty file")
|
||||
return False
|
||||
|
||||
all_errors = []
|
||||
warnings = []
|
||||
uuids = set()
|
||||
|
||||
# ========== 1. UUID Format Validation (UUIDv4) ==========
|
||||
print("\n[1/4] Checking UUID format (UUIDv4 compliance)...")
|
||||
uuid_errors = validate_uuids_format(content)
|
||||
if uuid_errors:
|
||||
all_errors.extend(uuid_errors)
|
||||
print(f" ❌ Found {len(uuid_errors)} invalid UUIDs")
|
||||
else:
|
||||
print(" ✅ All UUIDs are valid UUIDv4 format")
|
||||
|
||||
# ========== 2. UUID Duplicates Check ==========
|
||||
print("\n[2/4] Checking for duplicate UUIDs...")
|
||||
def check_uuid(node, path="root"):
|
||||
if isinstance(node, dict):
|
||||
if 'uuid' in node:
|
||||
uuid = node['uuid']
|
||||
if uuid in uuids:
|
||||
warnings.append(f"[DUPLICATE UUID] {uuid} found at {path}")
|
||||
else:
|
||||
uuids.add(uuid)
|
||||
|
||||
# Check for English descriptions (Basic Heuristic)
|
||||
if 'description' in node:
|
||||
desc = node['description']
|
||||
if isinstance(desc, str):
|
||||
if re.search(r'\bThe\b|\bThis\b|\bValue\b', desc):
|
||||
warnings.append(f"[POTENTIAL ENGLISH] at {path}: '{desc[:40]}...'")
|
||||
|
||||
for k, v in node.items():
|
||||
check_uuid(v, f"{path}.{k}")
|
||||
elif isinstance(node, list):
|
||||
for i, item in enumerate(node):
|
||||
check_uuid(item, f"{path}[{i}]")
|
||||
|
||||
check_uuid(content)
|
||||
dup_warnings = [w for w in warnings if 'DUPLICATE' in w]
|
||||
if dup_warnings:
|
||||
print(f" ⚠️ Found {len(dup_warnings)} duplicate UUIDs (warning only)")
|
||||
else:
|
||||
print(" ✅ No duplicate UUIDs")
|
||||
|
||||
# ========== 3. Graph Item References ==========
|
||||
print("\n[3/4] Checking graph item references...")
|
||||
item_keys = collect_item_keys(content)
|
||||
graph_ref_errors = validate_graph_references(content, item_keys)
|
||||
if graph_ref_errors:
|
||||
all_errors.extend(graph_ref_errors)
|
||||
print(f" ❌ Found {len(graph_ref_errors)} broken item references in graphs")
|
||||
else:
|
||||
print(f" ✅ All graph item references are valid ({len(item_keys)} items found)")
|
||||
|
||||
# ========== 4. Dashboard Graph References ==========
|
||||
print("\n[4/4] Checking dashboard graph references...")
|
||||
graph_names = collect_graph_names(content)
|
||||
dashboard_ref_errors = validate_dashboard_references(content, graph_names)
|
||||
if dashboard_ref_errors:
|
||||
all_errors.extend(dashboard_ref_errors)
|
||||
print(f" ❌ Found {len(dashboard_ref_errors)} broken graph references in dashboards")
|
||||
else:
|
||||
print(f" ✅ All dashboard graph references are valid ({len(graph_names)} graphs found)")
|
||||
|
||||
# ========== Summary ==========
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
if warnings:
|
||||
eng_warnings = [w for w in warnings if 'ENGLISH' in w]
|
||||
if eng_warnings:
|
||||
print(f"\n[WARNINGS] {len(eng_warnings)} potential English descriptions found (Arthur Audit)")
|
||||
for w in eng_warnings[:5]: # Show max 5
|
||||
print(f" • {w}")
|
||||
if len(eng_warnings) > 5:
|
||||
print(f" ... and {len(eng_warnings) - 5} more")
|
||||
|
||||
if all_errors:
|
||||
print(f"\n[ERRORS FOUND] {len(all_errors)} critical issues:")
|
||||
for e in all_errors:
|
||||
print(f" ❌ {e}")
|
||||
return False
|
||||
else:
|
||||
print("\n[SUCCESS] YAML Structure & UUIDs are VALID. ✅")
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def collect_uuids_from_file(file_path):
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = yaml.safe_load(f)
|
||||
if not content:
|
||||
return set()
|
||||
|
||||
local_uuids = set()
|
||||
def extract(node, path="root"):
|
||||
if isinstance(node, dict):
|
||||
if 'uuid' in node:
|
||||
# Ignore UUIDs in template_groups and host_groups (they SHOULD be shared)
|
||||
if "template_groups" not in path and "host_groups" not in path:
|
||||
local_uuids.add(node['uuid'])
|
||||
|
||||
for k, v in node.items():
|
||||
extract(v, f"{path}.{k}")
|
||||
|
||||
elif isinstance(node, list):
|
||||
for i, item in enumerate(node):
|
||||
extract(item, f"{path}[{i}]")
|
||||
extract(content)
|
||||
return local_uuids
|
||||
except Exception as e:
|
||||
print(f"[WARN] Could not parse {file_path} for collision check: {e}")
|
||||
return set()
|
||||
|
||||
def check_cross_template_collisions(target_file, search_dir):
|
||||
print(f"\n[INFO] Checking for cross-template UUID collisions in: {search_dir}")
|
||||
target_uuids = collect_uuids_from_file(target_file)
|
||||
if not target_uuids:
|
||||
return True # Target file is empty or invalid, handled by main validation
|
||||
|
||||
collisions = []
|
||||
|
||||
for root, _, files in os.walk(search_dir):
|
||||
for file in files:
|
||||
if file.endswith('.yaml') or file.endswith('.xml'):
|
||||
full_path = os.path.join(root, file)
|
||||
if os.path.abspath(full_path) == os.path.abspath(target_file):
|
||||
continue # Skip self
|
||||
|
||||
other_uuids = collect_uuids_from_file(full_path)
|
||||
intersection = target_uuids.intersection(other_uuids)
|
||||
|
||||
if intersection:
|
||||
for uuid in intersection:
|
||||
collisions.append(f"[COLLISION] UUID {uuid} exists in both '{os.path.basename(target_file)}' and '{file}'")
|
||||
|
||||
if collisions:
|
||||
print("\n[CROSS-TEMPLATE COLLISIONS DETECTED]:")
|
||||
for c in collisions:
|
||||
print(c)
|
||||
return False
|
||||
else:
|
||||
print("[SUCCESS] No cross-template UUID collisions found.")
|
||||
return True
|
||||
|
||||
def zabbix_import(file_path, url, token):
|
||||
print(f"\n[INFO] Attempting to import {os.path.basename(file_path)} to Zabbix at {url}...")
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
yaml_content = f.read()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Could not read file for import: {e}")
|
||||
return False
|
||||
|
||||
# Construct the JSON-RPC request for Zabbix 6.0/7.0
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "configuration.import",
|
||||
"params": {
|
||||
"format": "yaml",
|
||||
"source": yaml_content,
|
||||
"rules": {
|
||||
"host_groups": {
|
||||
"createMissing": True,
|
||||
"updateExisting": True
|
||||
},
|
||||
"template_groups": {
|
||||
"createMissing": True,
|
||||
"updateExisting": True
|
||||
},
|
||||
"templates": {
|
||||
"createMissing": True,
|
||||
"updateExisting": True
|
||||
},
|
||||
"valueMaps": {
|
||||
"createMissing": True,
|
||||
"updateExisting": True
|
||||
},
|
||||
"templateDashboards": {
|
||||
"createMissing": True,
|
||||
"updateExisting": True
|
||||
},
|
||||
"templateLinkage": {
|
||||
"createMissing": True, # Usually we want to link if missing
|
||||
"deleteMissing": False
|
||||
},
|
||||
"items": {
|
||||
"createMissing": True,
|
||||
"updateExisting": True,
|
||||
"deleteMissing": False
|
||||
},
|
||||
"discoveryRules": {
|
||||
"createMissing": True,
|
||||
"updateExisting": True,
|
||||
"deleteMissing": False
|
||||
},
|
||||
"triggers": {
|
||||
"createMissing": True,
|
||||
"updateExisting": True,
|
||||
"deleteMissing": False
|
||||
},
|
||||
"graphs": {
|
||||
"createMissing": True,
|
||||
"updateExisting": True,
|
||||
"deleteMissing": False
|
||||
},
|
||||
"httptests": {
|
||||
"createMissing": True,
|
||||
"updateExisting": True,
|
||||
"deleteMissing": False
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
|
||||
# Prepare request
|
||||
api_url = url.rstrip('/') + "/api_jsonrpc.php"
|
||||
headers = {
|
||||
'Content-Type': 'application/json-rpc',
|
||||
'Authorization': f'Bearer {token}'
|
||||
}
|
||||
data = json.dumps(payload).encode('utf-8')
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(api_url, data=data, headers=headers, method='POST')
|
||||
with urllib.request.urlopen(req) as response:
|
||||
resp_body = response.read().decode('utf-8')
|
||||
json_resp = json.loads(resp_body)
|
||||
|
||||
if 'error' in json_resp:
|
||||
error = json_resp['error']
|
||||
print(f"[IMPORT FAILED] API Error {error.get('code')}: {error.get('message')}")
|
||||
if 'data' in error:
|
||||
print(f"Details: {error['data']}")
|
||||
return False
|
||||
elif 'result' in json_resp and json_resp['result'] is True:
|
||||
print(f"[SUCCESS] Template imported successfully!")
|
||||
return True
|
||||
else:
|
||||
# Unexpected success response format, but likely success if no error
|
||||
print(f"[SUCCESS] Template imported (Response: {json_resp.get('result')})")
|
||||
return True
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"[IMPORT FAILED] HTTP Error: {e.code} - {e.reason}")
|
||||
return False
|
||||
except urllib.error.URLError as e:
|
||||
print(f"[IMPORT FAILED] Connection Error: {e.reason}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"[IMPORT FAILED] Unexpected error: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Validate and optionally import Zabbix templates.")
|
||||
parser.add_argument("file", help="Path to the YAML template file")
|
||||
parser.add_argument("--url", help="Zabbix Server URL (e.g., https://zabbix.example.com)", default=None)
|
||||
parser.add_argument("--token", help="Zabbix API Token", default=None)
|
||||
parser.add_argument("--import-template", action="store_true", help="Attempt to import if validation passes")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
file_path = args.file
|
||||
|
||||
# 1. Validate the file itself
|
||||
if not validate_yaml(file_path):
|
||||
sys.exit(1)
|
||||
|
||||
# 2. Check for collisions in the same directory (Gold Edition Suite)
|
||||
directory = os.path.dirname(os.path.abspath(file_path))
|
||||
if not check_cross_template_collisions(file_path, directory):
|
||||
sys.exit(1)
|
||||
|
||||
# 3. Import if requested
|
||||
if args.import_template:
|
||||
if not args.url or not args.token:
|
||||
print("\n[ERROR] To import, you must provide --url and --token.")
|
||||
sys.exit(1)
|
||||
|
||||
if not zabbix_import(file_path, args.url, args.token):
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
Loading…
Reference in New Issue