Initial commit
This commit is contained in:
37
drivers/__init__.py
Normal file
37
drivers/__init__.py
Normal file
@@ -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
|
||||
8
drivers/fiberhome.py
Normal file
8
drivers/fiberhome.py
Normal file
@@ -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).")
|
||||
326
drivers/nokia.py
Normal file
326
drivers/nokia.py
Normal file
@@ -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"
|
||||
Reference in New Issue
Block a user