Initial commit - Arthur Gold Standard: Tools, Templates e Documentação

This commit is contained in:
João Pedro Toledo Goncalves 2026-01-04 15:07:32 -03:00
commit 79e99e4768
30 changed files with 5397 additions and 0 deletions

51
.gitignore vendored Normal file
View File

@ -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

63
README.md Normal file
View File

@ -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.*

68
clean_hosts_export.py Normal file
View File

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

View File

@ -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

View File

@ -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}"
}
```

View File

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

View File

@ -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

View 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

View File

@ -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"

View File

@ -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

84
deploy_package/install.sh Normal file
View File

@ -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 "======================================================="

62
deploy_package/nginx.conf Normal file
View File

@ -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/*;
}

View File

@ -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

37
deploy_package/redis.conf Normal file
View File

@ -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

View File

@ -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');

201
deploy_package/zabbix.conf Normal file
View File

@ -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;
}
}

View File

@ -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!"

View File

@ -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
###############################################################

180
export_hosts_zabbix.py Normal file
View File

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

129
export_templates.py Normal file
View File

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

49
fix_ad_uuids.py Normal file
View File

@ -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).")

66
fix_uuids.py Normal file
View File

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

52
get_group_uuid.py Normal file
View File

@ -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")

232
merge_exchange.py Normal file
View File

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

38
regen_all.py Normal file
View File

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

29
regen_uuids.py Normal file
View File

@ -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.")

View File

@ -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**

View File

@ -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

493
validate_zabbix_template.py Normal file
View File

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