Initial commit

This commit is contained in:
HalbeBruno
2026-02-18 10:17:09 -03:00
commit 7a34121e6d
24 changed files with 2338 additions and 0 deletions

326
drivers/nokia.py Normal file
View 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"