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"