commit b264b583b86d2ba820cdae9d49a15a69fb34f5ef Author: HalbeBruno Date: Wed Feb 18 10:18:46 2026 -0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc4c584 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +venv/ +.env + +# Logs +*.log +flask.log + +# IDE +.vscode/ +.idea/ + +# Project Specific +hosts.json +implementation_plan.md +task.md +walkthrough.md +.DS_Store + +# Build & Dist +dist/ +verification/ +*.key +*.rkey diff --git a/.pyarmor/config b/.pyarmor/config new file mode 100644 index 0000000..6264b14 --- /dev/null +++ b/.pyarmor/config @@ -0,0 +1,6 @@ +[project] +src = /home/halbebruno/Projetos/ZabbixAPI + +[builder] +outer_keyname = license.key + diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f6af47 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# IPv0 OLT API (Middleware Zabbix) + +API RESTful intermediária desenvolvida em Python (Flask) para facilitar o monitoramento de OLTs (Nokia, Fiberhome, etc.) no Zabbix via Low Level Discovery (LLD). + +## 🎯 Objetivo +Transformar comandos complexos de CLI (Telnet/SSH) e estruturas SNMP proprietárias em JSON limpo e otimizado para o Zabbix, com cache inteligente para proteger a OLT de sobrecarga. + +## 🚀 Recursos Principais +- **Multi-Vendor**: Arquitetura baseada em drivers. Suporta Nokia (ISAM/FW específicos) com suporte futuro para Fiberhome, Huawei e ZTE. +- **Smart Cache**: Cache em memória evita múltiplas conexões para a mesma coleta de dados. + +## 📂 Estrutura do Projeto +``` +/ +├── app.py # Aplicação Principal (Flask) +├── drivers/ # Camada de abstração (Factory Pattern) +│ ├── __init__.py # Contrato (Interface OltDriver) +│ ├── nokia.py # Driver concreto Nokia (Implementa connect/get_pon_stats) +│ └── fiberhome.py # Placeholder para expansão futura +├── utils/ # Cache, SNMP, Helpers +├── tools/ # Scripts de Manutenção (Build, Clean, License) +└── doc/ # Documentação detalhada +``` + +## 📦 Instalação (Usuário Final) + +Para um guia detalhado, consulte: [doc/install_dist.md](doc/install_dist.md). + +1. Transfira o arquivo `ipv0-olt-api.zip` para o servidor (ex: `/opt`). +2. Descompacte o arquivo e entre na pasta: + ```bash + unzip ipv0-olt-api.zip -d ipv0-olt-api + cd ipv0-olt-api + ``` +3. Execute o script de instalação como root: + ```bash + sudo ./install.sh + ``` + *Este script instalará as dependências e configurará o serviço.* + +> ⚠️ **Atenção:** Ao final, ele exibirá o **Machine ID**. Copie este código para solicitar sua licença. + +## 🔌 Expansão (Novos Drivers/Recursos) +A API foi desenhada para ser estendida e suportar outros fabricantes. +Consulte o guia de desenvolvimento: [doc/expansion.md](doc/expansion.md). + +## 📊 Integrando no Zabbix +Para um guia passo-a-passo detalhado de como criar Master Items, regras LLD e protótipos de itens, consulte: [doc/zabbix_integration.md](doc/zabbix_integration.md). + +## 📚 Guia do Desenvolvedor +Documentação técnica para manutenção e evolução do projeto: + +* **Instalação (Ambiente Dev)**: [doc/install.md](doc/install.md) - *Como rodar o código fonte sem compilar.* +* **Expansão**: [doc/expansion.md](doc/expansion.md) - *Como criar novos drivers.* +* **Integração Zabbix (Guia Técnico)**: [doc/zabbix_integration.md](doc/zabbix_integration.md) - *Como criar templates e regras LLD.* +* **Build e Distribuição**: [doc/create_dist.md](doc/create_dist.md) - *Como gerar novas versões e licenças.* +* **Templates Oficiais**: + * **Nokia**: [doc/templates/template-nokia-api.xml](doc/templates/template-nokia-api.xml) + +--- +Desenvolvido com ❤️ e Python. \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..337c62e --- /dev/null +++ b/app.py @@ -0,0 +1,71 @@ +from flask import Flask, jsonify, request +from drivers.nokia import NokiaDriver +from drivers.fiberhome import FiberhomeDriver +from config import Config +from utils.cache import cache_response +import time + +import json +import os + +app = Flask(__name__) + +# Carregar inventário de hosts +HOSTS_CONFIG = {} +if os.path.exists('hosts.json'): + with open('hosts.json', 'r') as f: + HOSTS_CONFIG = json.load(f) + +# Mapeamento de drivers +DRIVERS = { + 'nokia': NokiaDriver, + 'fiberhome': FiberhomeDriver +} + +@app.route('/api/v1/olt_stats', methods=['GET']) +@cache_response # Cacheia a resposta baseado nos argumentos da request +def get_olt_stats(): + host_ip = request.args.get('host') + driver_name_param = request.args.get('driver') + + if not host_ip: + return jsonify({'error': 'Missing host parameter'}), 400 + + # Determinar configurações (hosts.json ou defaults) + host_config = HOSTS_CONFIG.get(host_ip, {}) + + # Parâmetros: Preference para query param > hosts.json > default config + driver_name = driver_name_param or host_config.get('driver') + username = host_config.get('username') or Config.OLT_USERNAME + password = host_config.get('password') or Config.OLT_PASSWORD + + # Opções extras para o driver (ex: ssh_options, snmp_community) + # Remove chaves padrão para não duplicar e deixa o resto como opção + driver_options = {k: v for k, v in host_config.items() if k not in ['username', 'password', 'driver']} + + if not driver_name: + return jsonify({'error': 'Driver not specified (param or hosts.json)'}), 400 + + if driver_name not in DRIVERS: + return jsonify({'error': f'Driver {driver_name} not supported'}), 400 + + try: + # Instancia o driver com opções extras + driver_class = DRIVERS[driver_name] + driver = driver_class(host_ip, username, password, **driver_options) + + # Coleta estatísticas completas (Cards > PONs > ONTs) + stats = driver.get_olt_stats() + + # Estrutura flexível, o driver define o retorno + return jsonify(stats) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/health', methods=['GET']) +def health_check(): + return jsonify({'status': 'ok', 'service': 'IPv0 OLT API', 'version': '3.1'}), 200 + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5050) diff --git a/config.py b/config.py new file mode 100644 index 0000000..8240dd8 --- /dev/null +++ b/config.py @@ -0,0 +1,16 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + # Credenciais padrão (podem ser sobrescritas por variáveis de ambiente) + OLT_USERNAME = os.getenv('OLT_USERNAME', 'admin') + OLT_PASSWORD = os.getenv('OLT_PASSWORD', 'admin') + + # Timeout do Netmiko em segundos + NETMIKO_TIMEOUT = int(os.getenv('NETMIKO_TIMEOUT', 10)) + + # TTL do Cache em segundos (padrão 5 minutos) + CACHE_TTL = int(os.getenv('CACHE_TTL', 300)) + CACHE_MAX_SIZE = int(os.getenv('CACHE_MAX_SIZE', 100)) diff --git a/debug_snmp.py b/debug_snmp.py new file mode 100644 index 0000000..afc5bf3 --- /dev/null +++ b/debug_snmp.py @@ -0,0 +1,51 @@ +import asyncio +from utils.snmp import _snmp_walk_async, SnmpEngine, CommunityData, UdpTransportTarget, ContextData, ObjectType, ObjectIdentity, get_cmd + +async def _snmp_get_async(host, community, oid): + snmp_engine = SnmpEngine() + community_data = CommunityData(community, mpModel=1) + # Using larger timeout/retries just in case + transport = await UdpTransportTarget.create((host, 161), timeout=2, retries=1) + context = ContextData() + + iterator = get_cmd( + snmp_engine, + community_data, + transport, + context, + ObjectType(ObjectIdentity(oid)) + ) + + errorIndication, errorStatus, errorIndex, varBinds = await iterator + snmp_engine.closeDispatcher() + + if errorIndication or errorStatus: + return None + return str(varBinds[0][1]) + +async def main(): + host = "10.186.203.14" + community = "public" + + # Hypothesis: Slot 6, Port 1 + # 0x06A00000 = 111149056 + + targets = [ + (5, 1, 94371840), # Validated + (6, 1, 111149056), # To Evaluate + (6, 2, 111149056 + 65536) + ] + + base_oid_prefix = "1.3.6.1.2.1.2.2.1.2" # ifDescr + + for slot, port, if_index in targets: + oid = f"{base_oid_prefix}.{if_index}" + print(f"Checking Slot {slot} Port {port} (ID={if_index})...") + val = await _snmp_get_async(host, community, oid) + if val and "No Such" not in val: + print(f" MATCH: {val}") + else: + print(f" FAIL: {val}") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/doc/create_dist.md b/doc/create_dist.md new file mode 100644 index 0000000..44b5c34 --- /dev/null +++ b/doc/create_dist.md @@ -0,0 +1,35 @@ +# Manual de Criação de Distribuição (Developer Guide) + +Este documento destina-se a geração de novas versões de distribuição da **API** e emissão delicenças. + +## 1. Gerando o Pacote de Distribuição +Para criar um pacote pronto para o cliente (binário protegido), execute: + +```bash +# Executar da raiz +./tools/build.sh +``` + +**Saída:** +* **Staging:** `dist/ipv0-olt-api/` (Arquivos soltos para conferência) +* **Release:** `dist/release/ipv0-olt-api.zip` (Arquivo final para o cliente) + +## 2. Gerenciamento de Licenças +A aplicação não rodará sem uma licença válida. Utilize o script automatizado para gerar e organizar licenças: + +```bash +./tools/gen_license.sh +``` + +O script solicitará: +1. **Nome do Cliente** (Cria pasta em `dist/licenses/CLIENTE`). +2. **Tipo de Licença** (Data ou Hardware). +3. **Dados** (Vencimento ou Machine ID). + +A licença gerada será salva em: `dist/licenses//license.key`. + +## 3. Atualizando a Versão +Ao modificar o código fonte: +1. Edite `config.py` ou features. +2. Rode `./build.sh`. +3. Envie o novo `release_production.zip` para o cliente. diff --git a/doc/expansion.md b/doc/expansion.md new file mode 100644 index 0000000..b6ca408 --- /dev/null +++ b/doc/expansion.md @@ -0,0 +1,112 @@ +# Guia de Expansão e Desenvolvimento + +Este documento explica como adicionar novos recursos (métodos) e suportar novos fabricantes (drivers) na API Zabbix OLT. + +## Arquitetura +A API segue um padrão de **Factory** simplificado: +1. **Driver Base (`drivers/__init__.py`)**: Define a interface comum (`OltDriver`). +2. **Implementações (`drivers/nokia.py`, etc)**: Classes concretas que herdam de `OltDriver`. +3. **API (`app.py`)**: Roteia as requisições para o método correto do driver instanciado. + +--- + +## 1. Adicionar Novo Tipo de Monitoramento (Ex: Interfaces) + +Para adicionar uma nova coleta de dados (ex: estatísticas de interface, temperatura, CPU): + +### Passo 1: Atualizar o Contrato +Edite `drivers/__init__.py` e adicione o método abstrato na classe `OltDriver`. + +```python +@abstractmethod +def get_interface_stats(self): + """Retorna estatísticas de interfaces Ethernet/Uplink/PON.""" + pass +``` + +### Passo 2: Implementar nos Drivers +Atualize **todas** as classes que herdam de `OltDriver` (ex: `drivers/nokia.py`). + +```python +def get_interface_stats(self): + connection = self.connect() + output = connection.send_command_timing("show interface ...") + + # Implementar lógica de parsing (regex) + stats = self._parse_interfaces(output) + + connection.disconnect() + return stats +``` + +### Passo 3: Criar Rota na API +Edite `app.py` para expor o novo método. + +```python +@app.route('/api/v1/interface_stats', methods=['GET']) +@cache_response +def get_interface_stats(): + # ... (Copiar lógica de instanciação do driver de get_pon_stats) ... + + try: + stats = driver.get_interface_stats() + return jsonify({"data": stats, "meta": ...}) + except Exception as e: + return jsonify({'error': str(e)}), 500 +``` + +--- + +## 2. Adicionar Novo Fabricante (Vendor) + +Para suportar uma nova OLT (ex: Huawei, ZTE): + +### Passo 1: Criar o Driver +Crie um arquivo em `drivers/` (ex: `drivers/huawei.py`). + +```python +from drivers import OltDriver +import re + +class HuaweiDriver(OltDriver): + def connect(self): + # Configurar conexão Netmiko (device_type='huawei') + return ConnectHandler(...) + + def get_pon_stats(self): + connection = self.connect() + output = connection.send_command("display ont info ...") + connection.disconnect() + return self._parse_output(output) +``` + +### Passo 2: Registrar o Driver +Edite `app.py` e importe a nova classe. + +```python +from drivers.huawei import HuaweiDriver + +DRIVERS = { + 'nokia': NokiaDriver, + 'fiberhome': FiberhomeDriver, + 'huawei': HuaweiDriver # <--- Novo registro +} +``` + +### Passo 3: Configurar Host +No `hosts.json`, defina o driver para o IP correspondente. + +```json +"10.10.10.1": { + "driver": "huawei", + ... +} +``` + +--- + +## Dicas de Desenvolvimento + +- **Netmiko Timing**: Use `send_command_timing` se o equipamento tiver prompts complexos ou lentidão. +- **Regex**: Teste suas expressões regulares com outputs reais variados para garantir robustez. +- **Cache**: O decorador `@cache_response` em `utils/cache.py` já trata cache por URL (host distinto = cache distinto). diff --git a/doc/install.md b/doc/install.md new file mode 100644 index 0000000..fbd42f4 --- /dev/null +++ b/doc/install.md @@ -0,0 +1,96 @@ +# Guia de Instalação: API Zabbix OLT + +Este documento descreve os passos para instalação e configuração da API em ambiente Linux. + +## Pré-requisitos +- Sistema Operacional Linux (Debian/Ubuntu/CentOS) +- Python 3.9 ou superior +- Git + +## Passos de Instalação + +### 1. Clonar o Repositório +O diretório padrão de instalação será `/opt/zbx-ipv0`. + +```bash +sudo mkdir -p /opt/zbx-ipv0 +sudo chown $USER:$USER /opt/zbx-ipv0 +git clone https://git.ipv0.com.br/halbebruno/zabbix-api /opt/zbx-ipv0 +cd /opt/zbx-ipv0 +``` + +### 2. Configurar Ambiente Virtual (venv) +É recomendado usar um ambiente virtual para isolar as dependências. + +```bash +python3 -m venv venv +source venv/bin/activate +``` + +### 3. Instalar Dependências +```bash +pip install -r requirements.txt +# Para produção, instale também o Gunicorn +pip install gunicorn +``` + +### 4. Configuração + +#### 4.1 Inventário de Hosts +Crie ou edite o arquivo `hosts.json` na raiz do projeto com as credenciais das OLTs. + +```json +{ + "10.186.203.14": { + "username": "admin", + "password": "senha_segura", + "driver": "nokia", + "port": 22, + "ssh_options": { + "disabled_algorithms": { + "pubkeys": ["rsa-sha2-256", "rsa-sha2-512"] + } + } + } +} +``` + +#### 4.2 Variáveis de Ambiente (Opcional) +Você pode sobrescrever configurações padrão via variáveis de ambiente ou editando `config.py`. + +### 5. Execução como Serviço (Systemd) +Para garantir que a API inicie automaticamente e rode em background, crie um serviço systemd. + +Crie o arquivo `/etc/systemd/system/zabbix-api.service`: + +```ini +[Unit] +Description=Zabbix OLT API +After=network.target + +[Service] +User=root +Group=root +WorkingDirectory=/opt/zbx-ipv0 +Environment="PATH=/opt/zbx-ipv0/venv/bin" +ExecStart=/opt/zbx-ipv0/venv/bin/gunicorn --workers 4 --bind 0.0.0.0:5000 app:app +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +Ative e inicie o serviço: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable zabbix-api +sudo systemctl start zabbix-api +sudo systemctl status zabbix-api +``` + +## Teste +Verifique se a API está rodando: +```bash +curl http://127.0.0.1:5000/api/v1/pon_stats?host=SEU_IP_DA_OLT +``` diff --git a/doc/install_dist.md b/doc/install_dist.md new file mode 100644 index 0000000..29f4ab7 --- /dev/null +++ b/doc/install_dist.md @@ -0,0 +1,86 @@ +# Manual de Instalação (Cliente) + +Este guia descreve como instalar a **IPv0 OLT API** em seu servidor Linux. + +## Pré-requisitos +* Linux (Ubuntu 20.04+, Debian 11+, CentOS 8+) +* Python 3.8 ou superior +* Acesso root/sudo + +## 1. Instalação Automática + +1. Transfira o arquivo `ipv0-olt-api.zip` para o servidor (ex: `/opt`). +2. Descompacte o arquivo: + ```bash + unzip ipv0-olt-api.zip -d ipv0-olt-api + cd ipv0-olt-api + ``` +3. Execute o script de instalação como root: + ```bash + sudo ./install.sh + ``` + * Este script instalará as dependências e configurará o serviço. + * **Atenção:** Ao final, ele exibirá o **Machine ID**. Copie este código para solicitar sua licença. + +## 2. Ativação da Licença e Início do Serviço + +A aplicação não iniciará sem uma licença válida. + +1. Envie o **Machine ID** para o suporte. +2. Você receberá um arquivo de licença chamado `license.key`. +3. Copie este arquivo para a pasta da aplicação e INICIE o serviço: + ```bash + # Copiar a licença + sudo cp license.key /opt/ipv0-olt-api/ + sudo chown root:root /opt/ipv0-olt-api/license.key + + # Habilitar e Iniciar o serviço agora: + sudo systemctl enable --now ipv0-olt-api + ``` + +## 3. Configuração (hosts.json) + +Edite o arquivo `hosts.json` para adicionar suas OLTs. +Caminho: `/opt/ipv0-olt-api/hosts.json` + +**Exemplo Completo:** +```json +{ + "10.155.156.2": { + "username": "zabbix", + "password": "p4ssw0rd", + "driver": "nokia", + "snmp_community": "public", + "port": 22, + "ssh_options": { + "disabled_algorithms": { + "pubkeys": [ + "rsa-sha2-256", + "rsa-sha2-512" + ] + } + } + }, + "192.168.1.10": { + "username": "admin", + "password": "simple_password", + "driver": "fiberhome", + "port": 23 + } +} +``` +Após editar, reinicie o serviço. + +## 4. Diagnóstico e Debug + +A API inclui uma ferramenta de diagnóstico. Para verificar status, licença e conexão: + +```bash +cd /opt/ipv0-olt-api +sudo ./venv/bin/python3 debug.py +``` + +### Comandos Úteis +* **Ver logs:** `sudo journalctl -u ipv0-olt-api -f` +* ** Status do Serviço:** `sudo systemctl status ipv0-olt-api` +* **Testar API:** `curl http://localhost:5050/health` diff --git a/doc/templates/template-nokia-api.xml b/doc/templates/template-nokia-api.xml new file mode 100644 index 0000000..87685f4 --- /dev/null +++ b/doc/templates/template-nokia-api.xml @@ -0,0 +1,945 @@ + + + 7.2 + + + b01bb96a1ed547e1a8770677a646aa3c + Templates IPv0 + + + 1a62bf6492d64062a2b5f58e30234244 + Templates Nokia + + + + + + diff --git a/doc/zabbix_integration.md b/doc/zabbix_integration.md new file mode 100644 index 0000000..d8ba120 --- /dev/null +++ b/doc/zabbix_integration.md @@ -0,0 +1,120 @@ +# Guia Técnico de Integração Zabbix + +Este documento descreve como construir templates Zabbix para consumir a **IPv0 OLT API**. Ele detalha os Headers, Regras de Descoberta (LLD), Pré-processamentos Java Script e estrutura dos itens. + +## 1. Visão Geral (Master Item) + +Toda a coleta de dados de uma OLT deve ser centralizada em um único **Master Item (HTTP Agent)**. Isso aproveita o cache da API e evita sobrecarga de conexões. + +### Configuração do Master Item +* **Nome:** `OLT API Stats` +* **Type:** `HTTP agent` +* **Key:** `olt.api.stats` +* **URL:** `http://{$API_IP}:{$API_PORT}/api/v1/olt_stats` +* **Query Fields:** + * `host`: `{HOST.CONN}` ou `{HOST.IP}` +* **Headers:** + * `Content-Type`: `application/json` +* **Timeout:** `30s` (ou mais, dependendo do tamanho da OLT) +* **History:** `1h` (NÃO guarde histórico longo do JSON bruto se for muito grande) +* **Trends:** `0` (Texto não tem trend) + +--- + +## 2. Padrão de Descoberta (Low Level Discovery - LLD) + +A API retorna um JSON estruturado hierarquicamente (`NGFC`, `FGLT`, etc). Para criar itens no Zabbix, precisamos "achatar" essa estrutura usando **LLD Dependent Rules**. + +### 2.1 LLD de Cards (Placas) +Para descobrir as placas (FGLT, FANT, etc): + +* **Type:** `Dependent item` +* **Master Item:** `OLT API Stats` +* **Key:** `olt.card.discovery` +* **Preprocessing:** + 1. **JavaScript**: + ```javascript + var data = JSON.parse(value); + var output = []; + // Itera sobre tipos de cards conhecidos + ['FGLT', 'FANT', 'NGFC'].forEach(function(type) { + if (data[type]) { + data[type].forEach(function(card) { + output.push({ + "{#CARD_INDEX}": card.cardIndex, + "{#CARD_NAME}": card.cardName, + "{#CARD_TYPE}": card.cardType, + "{#CARD_SLOT}": card.cardSlot || card.cardNumber // Ajuste conforme driver + }); + }); + } + }); + return JSON.stringify(output); + ``` + +### 2.2 LLD de PONs (Portas) +Para descobrir as portas PON e criar métricas de suporte (Total/Online/Offline): + +* **Type:** `Dependent item` +* **Master Item:** `OLT API Stats` +* **Key:** `olt.pon.discovery` +* **Preprocessing:** + 1. **JavaScript**: + ```javascript + var data = JSON.parse(value); + var output = []; + if (data.FGLT) { + data.FGLT.forEach(function(card) { + if (card.pons) { + card.pons.forEach(function(pon) { + output.push({ + "{#PON_NAME}": pon.ponName, + "{#PON_INDEX}": pon.ponIndex, + "{#PON_CODE}": pon.ponCode, // Importante para SNMP + "{#CARD_INDEX}": card.cardIndex + }); + }); + } + }); + } + return JSON.stringify(output); + ``` + +--- + +## 3. Protótipos de Itens (Item Prototypes) + +### 3.1 Métricas via JSON (Dependentes) +Métricas como "ONTs Online" vêm direto do JSON. Não use SNMP ou HTTP novo. Use **Dependent Item**. + +* **Name:** `PON {#PON_NAME}: ONTs Online` +* **Type:** `Dependent item` +* **Master Item:** `OLT API Stats` +* **Key:** `pon.online[{#PON_INDEX}]` +* **Preprocessing:** + 1. **JSONPath**: + ``` + $.FGLT[?(@.cardIndex=='{#CARD_INDEX}')].pons[?(@.ponIndex=='{#PON_INDEX}')].onuStats.up.first() + ``` + +### 3.2 Métricas via SNMP (SNMP Agent) +Métricas físicas (Temperatura, Voltagem, Status Operacional) devem ser coletadas via SNMP direto da OLT, usando os índices descobertos. + +* **Name:** `PON {#PON_NAME}: Temperatura` +* **Type:** `SNMP agent` +* **Key:** `pon.temp[{#PON_INDEX}]` +* **SNMP OID:** + * Exemplo Nokia: `1.3.6.1.4.1.637.61.1.56.5.1.10.{#CARD_INDEX}.{#PON_INDEX}` + * Exemplo Interface Genérica: `1.3.6.1.2.1.2.2.1.8.{#PON_CODE}` + +--- + +## 4. Templates Disponíveis + +Abaixo listamos os templates XML prontos para importação que seguem este padrão: + +* **OLT Nokia**: [doc/templates/template-nokia-api.xml](doc/templates/template-nokia-api.xml) + * *Suporta: 7360 ISAM, Placas FGLT/FANT, Métricas de ONTs, Status Físico (SFP).* + +--- +**Nota:** Mantenha os templates versionados junto com o código da API para garantir compatibilidade com as estruturas JSON retornadas. diff --git a/drivers/__init__.py b/drivers/__init__.py new file mode 100644 index 0000000..ab15d02 --- /dev/null +++ b/drivers/__init__.py @@ -0,0 +1,37 @@ +from abc import ABC, abstractmethod + +class OltDriver(ABC): + """ + Classe base abstrata para drivers de OLT. + Todos os drivers específicos devem herdar desta classe e implementar + seus métodos abstratos. + """ + def __init__(self, host, username, password, **kwargs): + self.host = host + self.username = username + self.password = password + self.options = kwargs + + @abstractmethod + def connect(self): + """ + Estabelece a conexão com a OLT (SSH/Telnet). + Deve retornar o objeto de conexão (ex: Netmiko connection handler). + """ + pass + + @abstractmethod + def get_olt_stats(self): + """ + Coleta estatísticas completas da OLT (Cards > PONs > ONTs). + Deve retornar um dicionário hierárquico: + { + "FGLT": [ + { + "cardIndex": "...", + "pons": [...] + } + ] + } + """ + pass diff --git a/drivers/fiberhome.py b/drivers/fiberhome.py new file mode 100644 index 0000000..9a5abbd --- /dev/null +++ b/drivers/fiberhome.py @@ -0,0 +1,8 @@ +from drivers import OltDriver + +class FiberhomeDriver(OltDriver): + def connect(self): + raise NotImplementedError("Driver Fiberhome ainda não implementado.") + + def get_olt_stats(self): + raise NotImplementedError("Driver Fiberhome ainda não implementado (v2.0).") diff --git a/drivers/nokia.py b/drivers/nokia.py new file mode 100644 index 0000000..f535fc8 --- /dev/null +++ b/drivers/nokia.py @@ -0,0 +1,326 @@ +from netmiko import ConnectHandler +from drivers import OltDriver +from utils.snmp import snmp_walk +from cachetools import TTLCache, cached +import re +import time + +# Cache de estrutura (Cards/PONs) persistente por 1 hora +# Chave: IP do host +_structure_cache = TTLCache(maxsize=100, ttl=3600) + +class NokiaDriver(OltDriver): + def connect(self): + device = { + 'device_type': 'nokia_sros', + 'host': self.host, + 'username': self.username, + 'password': self.password, + } + + # Merge options (ssh_options, etc) + if self.options: + # Tratamento especial para ssh_options + if 'ssh_options' in self.options: + device.update(self.options['ssh_options']) + # Adiciona o restante das opções direto no device, excluindo snmp_community + device.update({k: v for k, v in self.options.items() if k not in ['ssh_options', 'snmp_community']}) + + device.setdefault('global_delay_factor', 2) + + return ConnectHandler(**device) + + def get_olt_stats(self): + start_time = time.time() + print(f"[{self.host}] Starting collection...", flush=True) + + # 1. Obter Estatísticas via CLI + cli_stats = self._get_cli_stats() + print(f"[{self.host}] CLI stats collected in {time.time() - start_time:.2f}s", flush=True) + + t_snmp = time.time() + # 2. Obter Estrutura via SNMP (Descoberta de Hardware) - Agora com Cache de 1h + structure = self._get_snmp_structure() + + if structure is None: + print(f"[{self.host}] CRITICAL: SNMP structure is None!", flush=True) + structure = {} + + print(f"[{self.host}] SNMP structure keys: {list(structure.keys())} (Time: {time.time() - t_snmp:.2f}s)", flush=True) + + # 3. Merge dos dados + merged = self._merge_data(structure, cli_stats) + + if merged is None: + print(f"[{self.host}] CRITICAL: Merged data is None!", flush=True) + merged = {} + + print(f"[{self.host}] Total execution time: {time.time() - start_time:.2f}s", flush=True) + return merged + + def _get_cli_stats(self): + try: + # O comando 'match' falhou (invalid token). + # Revertendo para o método '?' que sabemos que funciona (bypassa paginação). + + device = { + 'device_type': 'nokia_sros', + 'host': self.host, + 'username': self.username, + 'password': self.password, + } + if self.options and 'ssh_options' in self.options: + device.update(self.options['ssh_options']) + + # Recriando conexão para garantir estado limpo (send_command_timing exige cuidado) + conn = ConnectHandler(**device) + + cmd = "show interface port ?" + print(f"[{self.host}] Sending CLI cmd: {cmd}", flush=True) + + output = conn.send_command_timing(cmd, last_read=2.0) + conn.disconnect() + + print(f"[{self.host}] CLI output length: {len(output)} chars", flush=True) + parsed = self._parse_cli_output(output) + print(f"[{self.host}] Parsed clean ONTs count: {sum(len(v['onuStats']) for v in ([],) if False) or sum(len(x) for x in parsed.values()) if parsed else 0}", flush=True) + + return parsed + except Exception as e: + print(f"CLI Error: {e}. Falling back...", flush=True) + return self._fallback_cli_stats() + + def _fallback_cli_stats(self): + try: + device = { + 'device_type': 'nokia_sros', + 'host': self.host, + 'username': self.username, + 'password': self.password, + } + if self.options and 'ssh_options' in self.options: + device.update(self.options['ssh_options']) + + conn = ConnectHandler(**device) + output = conn.send_command_timing("show interface port", last_read=2.0) + conn.disconnect() + return self._parse_cli_output(output) + except Exception as ex: + print(f"Fallback CLI Error: {ex}") + return {} + + def _parse_cli_output(self, output): + stats = {} + # Regex ajustado para pegar apenas ONTs + regex_ont = re.compile(r'(ont:\d+/\d+/\d+/\d+/\d+)\s+(\S+)\s+(\S+)') + + lines = output.splitlines() + + for line in lines: + line = line.strip() + + match_ont = regex_ont.search(line) + if match_ont: + ont_full = match_ont.group(1) + ont_id_full = ont_full.replace("ont:", "") + parts = ont_id_full.split("/") + if len(parts) > 1: + pon_index = "/".join(parts[:-1]) + if pon_index not in stats: + stats[pon_index] = {"online_onts": 0, "offline_onts": 0, "total_onts": 0} + + stats[pon_index]["total_onts"] += 1 + oper = match_ont.group(3).lower() + if "up" in oper: + stats[pon_index]["online_onts"] += 1 + else: + stats[pon_index]["offline_onts"] += 1 + return stats + + @cached(_structure_cache, key=lambda self: self.host) + def _get_snmp_structure(self): + community = self.options.get('snmp_community', 'public') + host = self.host + cards_oid = '1.3.6.1.4.1.637.61.1.23.3.1.3' + + try: + card_results = snmp_walk(host, community, cards_oid) + except Exception as e: + print(f"SNMP Error (Cards): {e}") + return {} + + structure = {} + seq_counters = {} + + for oid, value in card_results: + card_index = oid.split('.')[-1] + raw_type = value.replace('"', '').strip() + + if raw_type == 'EMPTY' or not raw_type: + continue + + if '-' in raw_type: + card_type, card_class = raw_type.split('-', 1) + else: + card_type, card_class = raw_type, '' + + if card_type not in seq_counters: + seq_counters[card_type] = 0 + seq_counters[card_type] += 1 + card_seq = seq_counters[card_type] + + # Construção do objeto Card mantendo a ordem desejada das chaves + card_obj = { + "cardClass": card_class, + "cardIndex": card_index, + "cardName": f"CARD {card_seq}", + "cardNumber": card_seq, + "cardType": card_type, + "pons": [] + } + + if card_type == 'FGLT': + # Pega PONs (estrutura apenas) + pons = self._get_snmp_pons(host, community, card_index, card_seq) + card_obj['pons'] = pons + + if card_type not in structure: + structure[card_type] = [] + structure[card_type].append(card_obj) + + return structure + + def _get_snmp_pons(self, host, community, card_index, card_seq): + base_oid = f'1.3.6.1.4.1.637.61.1.56.5.1.3.{card_index}' + try: + pon_results = snmp_walk(host, community, base_oid) + except: + return [] + + pons = [] + for oid, value in pon_results: + pon_index = oid.split('.')[-1] + pon_code = value + pon_name = f"1/1/{card_seq}/{pon_index}" + + # Objeto PON provisório (será recriado no merge para ordem final) + pons.append({ + "cardIndex": card_index, # Necessário para lógica interna, será removido no final + "ponIndex": pon_index, + "ponName": pon_name, + "ponCode": pon_code, + "onuStats": { + "up": "0", "down": "0", "total": "0" + } + }) + + return pons + + def _merge_data(self, structure, cli_stats): + if not structure: + return structure + + cli_slots_map = {} + for k, v in cli_stats.items(): + parts = k.split('/') + if len(parts) >= 4: + slot = parts[2] + port = parts[3] + if slot not in cli_slots_map: + cli_slots_map[slot] = {} + cli_slots_map[slot][port] = v + + sorted_cli_slots = sorted(cli_slots_map.keys(), key=lambda x: int(x) if x.isdigit() else x) + + # 1. Ordenação Top-Level: NGFC, FANT, FGLT + ordered_structure = {} + for type_key in ["NGFC", "FANT", "FGLT"]: + if type_key in structure: + ordered_structure[type_key] = structure[type_key] + # Adiciona quaisquer outros tipos que não estejam na lista prioritária + for k, v in structure.items(): + if k not in ordered_structure: + ordered_structure[k] = v + + if 'FGLT' in ordered_structure: + fglt_cards = ordered_structure['FGLT'] + fglt_cards.sort(key=lambda x: int(x['cardIndex']) if x['cardIndex'].isdigit() else x['cardIndex']) + + print(f"DEBUG: Processing {len(fglt_cards)} FGLT cards. CLI Slots: {sorted_cli_slots}") + + for i, card in enumerate(fglt_cards): + real_slot = "0" + slot_stats = {} + + if i < len(sorted_cli_slots): + real_slot = sorted_cli_slots[i] + if real_slot in cli_slots_map: + slot_stats = cli_slots_map[real_slot] + else: + # Fallback para cards sem CLI stats + # Empírico: Cards começam no slot 5 (ou 1?) + # card['cardNumber'] 1 -> Slot 5? + real_slot = str(card['cardNumber'] + 4) + + print(f"DEBUG: Card {card['cardName']} (seq {card['cardNumber']}) mapped to Slot {real_slot}") + + # Iterar sobre PONs + for j, pon in enumerate(card['pons']): + p_idx = pon['ponIndex'] + + # Valores default + up, down, total = "0", "0", "0" + + if p_idx in slot_stats: + s = slot_stats[p_idx] + up = str(s['online_onts']) + down = str(s['offline_onts']) + total = str(s['total_onts']) + + # CALCULAR PON CODE (IF-INDEX) + # Se real_slot for válido (não "0"), calculamos. + # Se for "0" (impossível com fallback acima), mantemos original. + + final_code = pon['ponCode'] + if real_slot != "0": + # PATCH: Correção para Slot 6 (FGLT) que mapeia para 0x07... + # Parece haver um offset de +1 a partir do Slot 6 (ou específico dele) + calc_slot = real_slot + if str(real_slot) == "6": + calc_slot = "7" + print(f"DEBUG: Slot 6 detected. Applying correction -> 7 for ifIndex calc.", flush=True) + + final_code = self._calculate_if_index(calc_slot, p_idx) + + # Debug para comparar valor SNMP original vs Calculado + if pon['ponCode'] and pon['ponCode'] != final_code: + print(f"DEBUG: Code Mismatch for {pon['ponName']}! SNMP={pon['ponCode']} Calc={final_code}", flush=True) + + new_pon_obj = { + "ponCode": final_code, + "ponIndex": p_idx, + "ponName": f"PON 1/1/{real_slot}/{p_idx}", + "onuStats": { + "down": down, + "total": total, + "up": up + } + } + card['pons'][j] = new_pon_obj + + return ordered_structure + + def _calculate_if_index(self, slot, port): + """ + Calcula o if-index baseado no Slot e Porta. + Engenharia reversa: + Slot 5, Port 1 -> 94371840 (0x05A00000) + Slot 6, Port 1 -> 127926272 (0x07A00000) -> Fix: Usar Slot 7 calc + Fórmula: (Slot << 24) + ((159 + Port) << 16) + """ + try: + s = int(slot) + p = int(port) + return str((s << 24) + ((159 + p) << 16)) + except: + return "0" diff --git a/hosts.example.json b/hosts.example.json new file mode 100644 index 0000000..0713a09 --- /dev/null +++ b/hosts.example.json @@ -0,0 +1,13 @@ +{ + "10.0.0.1": { + "username": "admin", + "password": "change_me", + "driver": "nokia", + "port": 22, + "ssh_options": { + "disabled_algorithms": { + "pubkeys": ["rsa-sha2-256", "rsa-sha2-512"] + } + } + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..014cc8d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Flask==3.0.3 +netmiko==4.3.0 +cachetools==5.3.3 +python-dotenv==1.0.1 +pysnmp +pyarmor diff --git a/tools/build.sh b/tools/build.sh new file mode 100755 index 0000000..5cd442a --- /dev/null +++ b/tools/build.sh @@ -0,0 +1,42 @@ +# Definir diretórios (Relativos à raiz do projeto, assumindo execução via ./tools/build.sh) +# Mas vamos garantir que o script rode da raiz +cd "$(dirname "$0")/.." + +echo "[BUILD] Limpando builds anteriores..." +rm -rf dist build release_production.zip + +# Definir diretórios +STAGE_DIR="dist/ipv0-olt-api" +RELEASE_DIR="dist/release" +mkdir -p $STAGE_DIR +mkdir -p $RELEASE_DIR + +echo "[BUILD] Iniciando Obfuscação Pyarmor (Modo Full)..." +# Usando Pyarmor para proteger todo o código fonte +# O comando gen irá usar a configuração existente em .pyarmor se houver, ou criar uma nova. +# --outer: Permite usar chave de licença externa (license.key via outer_keyname=license.key) +./venv/bin/pyarmor gen --outer -O $STAGE_DIR app.py drivers/ utils/ config.py + + +echo "[BUILD] Copiando arquivos estáticos..." +cp hosts.json $STAGE_DIR/ +cp requirements.txt $STAGE_DIR/ +cp README.md $STAGE_DIR/ +cp tools/debug.py $STAGE_DIR/ +cp tools/service/install.sh $STAGE_DIR/ +cp tools/service/ipv0-olt-api.service $STAGE_DIR/ +chmod +x $STAGE_DIR/install.sh + +echo "[BUILD] Criando arquivo ZIP (via Python)..." +# Usar Python para zipar o CONTEÚDO do diretório de staging para dentro do zip +# O zip final ficará em dist/release/ipv0-olt-api.zip +# Estando em dist/ipv0-olt-api, ../release aponta para dist/release +cd $STAGE_DIR +../../venv/bin/python3 -c "import shutil; shutil.make_archive('../release/ipv0-olt-api', 'zip', '.')" +cd ../.. + +echo "✅ Build Process Complete!" +echo "Artifacts:" +echo " - Staging: $STAGE_DIR" +echo " - Release: $RELEASE_DIR/ipv0-olt-api.zip" +ls -F $RELEASE_DIR/ diff --git a/tools/debug.py b/tools/debug.py new file mode 100644 index 0000000..765cea2 --- /dev/null +++ b/tools/debug.py @@ -0,0 +1,51 @@ +import os +import sys +import subprocess +import time +import urllib.request +import json + +def print_header(msg): + print(f"\n{'='*40}\n {msg}\n{'='*40}") + +import glob + +def check_pyarmor_binding(): + print_header("Hardware & License (Machine ID)") + + try: + # Usar ferramenta oficial do Pyarmor CLI para HD Info + subprocess.run([sys.executable, "-m", "pyarmor.cli.hdinfo"], check=False) + except Exception as e: + print(f"Erro ao consultar HD Info: {e}") + +def check_service_status(): + print_header("Service Status (systemd)") + ret = subprocess.run(["systemctl", "status", "ipv0-olt-api", "--no-pager"], capture_output=False) + if ret.returncode != 0: + print("⚠️ Serviço parece estar parado ou com erro.") + +def check_api_health(): + print_header("API Health Check") + url = "http://localhost:5050/health" + try: + print(f"Connecting to {url}...") + with urllib.request.urlopen(url, timeout=5) as response: + if response.status == 200: + data = json.loads(response.read().decode()) + print(f"✅ API Online! Status: {data}") + else: + print(f"❌ API retornou status code: {response.status}") + except Exception as e: + print(f"❌ Falha na conexão com API: {e}") + print("Verifique se o serviço está rodando e se a licença é válida.") + +if __name__ == "__main__": + print("IPv0 OLT API - Debug Tool (v3.1)") + + check_service_status() + check_pyarmor_binding() + check_api_health() + + print("\nLogs recentes:") + subprocess.run(["journalctl", "-u", "ipv0-olt-api", "-n", "10", "--no-pager"], check=False) diff --git a/tools/gen_license.sh b/tools/gen_license.sh new file mode 100755 index 0000000..6c10db6 --- /dev/null +++ b/tools/gen_license.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Ensure we are in project root +cd "$(dirname "$0")/.." + +# Configuration +OUTPUT_BASE="dist/licenses" +PYARMOR="./venv/bin/pyarmor" + +# Colors (only if terminal supports it, otherwise empty) +if [ -t 1 ]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + BLUE='\033[0;34m' + NC='\033[0m' +else + RED='' + GREEN='' + BLUE='' + NC='' +fi + +echo "${BLUE}=== Gerador de Licenças IPv0 OLT API ===${NC}" + +# 1. Solicitar Nome do Cliente +echo "Nome do Cliente (sem espaços, ex: ProvedorX): " +read CLIENT_NAME +if [ -z "$CLIENT_NAME" ]; then + echo "${RED}Erro: Nome do cliente é obrigatório.${NC}" + exit 1 +fi + +DEST_DIR="$OUTPUT_BASE/$CLIENT_NAME" +mkdir -p "$DEST_DIR" + +# 2. Escolher Tipo de Licença +echo "" +echo "Tipo de Licença:" +echo "1) Data de Expiração (Trial/PoC)" +echo "2) Hardware (Produção/Machine ID)" +echo "Opção [1/2]: " +read OPTION + +if [ "$OPTION" = "1" ]; then + # Licença por Data + echo "Data de Vencimento (YYYY-MM-DD): " + read EXPIRE_DATE + # Validação simples de formato YYYY-MM-DD + if ! echo "$EXPIRE_DATE" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then + echo "${RED}Erro: Formato de data inválido.${NC}" + exit 1 + fi + + echo "Gerando licença Trial para ${GREEN}$CLIENT_NAME${NC} até ${GREEN}$EXPIRE_DATE${NC}..." + $PYARMOR gen key -e "$EXPIRE_DATE" + +elif [ "$OPTION" = "2" ]; then + # Licença por Hardware + echo "Machine ID do Cliente: " + read MACHINE_ID + if [ -z "$MACHINE_ID" ]; then + echo "${RED}Erro: Machine ID é obrigatório.${NC}" + exit 1 + fi + + echo "Gerando licença Permanente para ${GREEN}$CLIENT_NAME${NC} (ID: $MACHINE_ID)..." + $PYARMOR gen key --bind-device "$MACHINE_ID" + +else + echo "${RED}Opção inválida.${NC}" + exit 1 +fi + +# 3. Mover e Verifica +# Por padrão, o pyarmor gera em dist/license.key (conforme config outer_keyname) +GENERATED_FILE="dist/license.key" + +if [ -f "$GENERATED_FILE" ]; then + mv "$GENERATED_FILE" "$DEST_DIR/" + echo "" + echo "${GREEN}✅ Licença gerada com sucesso!${NC}" + echo "Arquivo: ${BLUE}$DEST_DIR/license.key${NC}" + echo "Envie este arquivo para o cliente." +else + echo "${RED}❌ Erro: O arquivo de licença não foi gerado.${NC}" + exit 1 +fi diff --git a/tools/service/install.sh b/tools/service/install.sh new file mode 100644 index 0000000..7f4c35b --- /dev/null +++ b/tools/service/install.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# IPv0 OLT API Installer +# Usage: sudo ./install.sh + +if [ "$EUID" -ne 0 ]; then + echo "Please run as root (sudo ./install.sh)" + exit 1 +fi + +DEST_DIR="/opt/ipv0-olt-api" + +echo "[INSTALL] Installing dependencies..." +apt-get update +apt-get install -y python3-venv python3-pip unzip + +echo "[INSTALL] Setting up directory..." +mkdir -p $DEST_DIR +# Copiar arquivos do diretório atual para o destino (se não estiver lá) +if [ "$PWD" != "$DEST_DIR" ]; then + cp -r * $DEST_DIR/ +fi + +cd $DEST_DIR + +echo "[INSTALL] Creating virtual environment..." +python3 -m venv venv +./venv/bin/pip install -r requirements.txt + +echo "[INSTALL] Configuring Service..." +cp ipv0-olt-api.service /etc/systemd/system/ +systemctl daemon-reload +# Não habilitar/iniciar automaticamente, pois falta a licença +# systemctl enable ipv0-olt-api +# systemctl start ipv0-olt-api + +echo "" +echo "[INSTALL] Dependencies installed!" + +# Obter Machine ID de forma limpa +MACHINE_ID=$(./venv/bin/python3 -m pyarmor.cli.hdinfo 2>/dev/null | grep "Machine ID" | cut -d: -f2 | xargs) + +# Cores +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "" +echo "Serviço 'ipv0-olt-api' configurado mas não iniciado - ${RED}PRECISA SER LICENCIADO${NC}." +echo "" +echo -e "Machine ID: ${YELLOW}${MACHINE_ID}${NC}" +echo "" + +echo "NEXT STEPS:" +echo "1. Solicite sua licença." +echo "2. Copie a licença 'license.key' para /opt/ipv0-olt-api/" +echo "3. Inicialize o serviço: sudo systemctl enable --now ipv0-olt-api" +echo "4. Após licenciar e inicializar o serviço, verifique se a api está funcionando com:" +echo " curl http://localhost:5050/health" diff --git a/tools/service/ipv0-olt-api.service b/tools/service/ipv0-olt-api.service new file mode 100644 index 0000000..9747bcd --- /dev/null +++ b/tools/service/ipv0-olt-api.service @@ -0,0 +1,14 @@ +[Unit] +Description=IPv0 OLT API (Zabbix Integration) +After=network.target + +[Service] +User=root +Group=root +WorkingDirectory=/opt/ipv0-olt-api +ExecStart=/opt/ipv0-olt-api/venv/bin/python3 app.py +Restart=always +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target diff --git a/utils/cache.py b/utils/cache.py new file mode 100644 index 0000000..2e173a1 --- /dev/null +++ b/utils/cache.py @@ -0,0 +1,27 @@ +from cachetools import TTLCache, cached +from config import Config +from flask import request + +# Criação do cache com TTL (Time To Live) +# O tamanho máximo e o tempo de vida são configurados via config.py +ttl_cache = TTLCache(maxsize=Config.CACHE_MAX_SIZE, ttl=Config.CACHE_TTL) + +def get_cache_key(*args, **kwargs): + """ + Gera uma chave única para o cache baseada na URL completa da requisição. + Isso garante que query parameters diferentes (host, driver) gerem entradas diferentes. + """ + if request: + return request.url + return str(args) + str(kwargs) + +def cache_response(func): + """ + Decorador wrapper para aplicar cache nas chamadas de função. + A chave do cache será baseada na URL da request. + """ + # A função wrapper precisa aceitar args/kwargs, mas a chave é gerada por get_cache_key + @cached(cache=ttl_cache, key=get_cache_key) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper diff --git a/utils/snmp.py b/utils/snmp.py new file mode 100644 index 0000000..b28ec9c --- /dev/null +++ b/utils/snmp.py @@ -0,0 +1,41 @@ +import asyncio +from pysnmp.hlapi.v3arch.asyncio import * + +async def _snmp_walk_async(host, community, oid): + results = [] + + # Configuração do Engine e dos dados de conexão + snmp_engine = SnmpEngine() + community_data = CommunityData(community, mpModel=1) # mpModel=1 para SNMPv2c + transport = await UdpTransportTarget.create((host, 161), timeout=2, retries=1) + context = ContextData() + + # Realiza o Walk + iterator = walk_cmd( + snmp_engine, + community_data, + transport, + context, + ObjectType(ObjectIdentity(oid)), + lexicographicMode=False + ) + + async for errorIndication, errorStatus, errorIndex, varBinds in iterator: + if errorIndication: + print(f"SNMP Error: {errorIndication}") + break + elif errorStatus: + print(f"SNMP Error: {errorStatus.prettyPrint()}") + break + else: + for varBind in varBinds: + results.append((str(varBind[0]), str(varBind[1]))) + + snmp_engine.closeDispatcher() + return results + +def snmp_walk(host, community, oid): + """ + Wrapper síncrono para o walk assíncrono. + """ + return asyncio.run(_snmp_walk_async(host, community, oid))