const { Client } = require('ssh2'); const config = require('../config/config'); class SSHManager { constructor() { this.connections = new Map(); this.equipamentos = new Map(); // Equipamentos pré-configurados (exemplo) this.setupDefaultEquipments(); } setupDefaultEquipments() { // Adicionar equipamentos padrão se configurados if (config.ssh.defaultHost) { this.equipamentos.set('default', { name: 'Equipamento Padrão', host: config.ssh.defaultHost, username: config.ssh.defaultUser, password: config.ssh.defaultPassword, port: 22 }); } // Concentrador Juniper principal this.equipamentos.set('juniper', { name: 'Concentrador Juniper', host: '131.0.192.254', username: 'rodrigo', password: '@102030Rod', port: 22, type: 'juniper' }); // Aliases para o concentrador this.equipamentos.set('concentrador', { name: 'Concentrador Juniper', host: '131.0.192.254', username: 'rodrigo', password: '@102030Rod', port: 22, type: 'juniper' }); // Exemplos de equipamentos (você pode configurar mais) this.equipamentos.set('router1', { name: 'Router Principal', host: '192.168.1.1', username: 'admin', password: 'admin', port: 22 }); this.equipamentos.set('switch1', { name: 'Switch Core', host: '192.168.1.10', username: 'admin', password: 'admin', port: 22 }); // OLTs para verificação de sinal this.equipamentos.set('olt_dito', { name: 'OLT DITO', host: '192.168.190.7', username: 'admin', password: 'admin', port: 22, type: 'olt' }); this.equipamentos.set('olt_colmeia', { name: 'OLT COLMEIA', host: '192.168.190.8', username: 'admin', password: 'admin', port: 22, type: 'olt' }); this.equipamentos.set('olt_canaa', { name: 'OLT CANAA', host: '192.168.190.9', username: 'admin', password: 'admin', port: 22, type: 'olt' }); this.equipamentos.set('olt_canaa2', { name: 'OLT CANAA 2', host: '192.168.190.10', username: 'admin', password: 'admin', port: 22, type: 'olt' }); this.equipamentos.set('olt_pedro_afonso', { name: 'OLT PEDRO AFONSO 1', host: '192.168.100.2', username: 'admin', password: 'admin', port: 22, type: 'olt' }); this.equipamentos.set('olt_canavieiras', { name: 'OLT CANAVIEIRAS', host: '192.168.100.4', username: 'admin', password: 'admin', port: 22, type: 'olt' }); } /** * Conecta via SSH a um equipamento * @param {string} equipmentId - ID do equipamento * @param {Object} customConfig - Configuração customizada (opcional) * @returns {Promise} - Promise com resultado da conexão */ connect(equipmentId, customConfig = null) { return new Promise((resolve, reject) => { const equipment = customConfig || this.equipamentos.get(equipmentId); if (!equipment) { reject(new Error(`Equipamento '${equipmentId}' não encontrado`)); return; } const conn = new Client(); const connectionId = `${equipmentId}_${Date.now()}`; conn.on('ready', () => { this.connections.set(connectionId, { connection: conn, equipment: equipment, connected: true, connectedAt: new Date() }); resolve({ success: true, connectionId: connectionId, message: `Conectado ao ${equipment.name} (${equipment.host})` }); }); conn.on('error', (err) => { reject(new Error(`Erro ao conectar: ${err.message}`)); }); conn.on('end', () => { this.connections.delete(connectionId); }); // Conectar conn.connect({ host: equipment.host, port: equipment.port || 22, username: equipment.username, password: equipment.password, readyTimeout: config.ssh.timeout }); }); } /** * Executa um comando SSH * @param {string} connectionId - ID da conexão * @param {string} command - Comando a executar * @returns {Promise} - Promise com resultado do comando */ executeCommand(connectionId, command) { return new Promise((resolve, reject) => { const connection = this.connections.get(connectionId); if (!connection || !connection.connected) { reject(new Error('Conexão não encontrada ou desconectada')); return; } connection.connection.exec(command, (err, stream) => { if (err) { reject(new Error(`Erro ao executar comando: ${err.message}`)); return; } let output = ''; let errorOutput = ''; stream.on('data', (data) => { output += data.toString(); }); stream.stderr.on('data', (data) => { errorOutput += data.toString(); }); stream.on('close', (code, signal) => { resolve({ success: code === 0, output: output.trim(), error: errorOutput.trim(), exitCode: code, signal: signal }); }); }); }); } /** * Desconecta uma conexão SSH * @param {string} connectionId - ID da conexão */ disconnect(connectionId) { const connection = this.connections.get(connectionId); if (connection) { connection.connection.end(); this.connections.delete(connectionId); return true; } return false; } /** * Testa conectividade com um equipamento * @param {string} equipmentId - ID do equipamento * @returns {Promise} - Promise com resultado do teste */ async testConnection(equipmentId) { try { const result = await this.connect(equipmentId); // Executar comando simples para testar const cmdResult = await this.executeCommand(result.connectionId, 'echo "test"'); // Desconectar this.disconnect(result.connectionId); return { success: true, message: `Conexão com ${equipmentId} OK`, details: cmdResult }; } catch (error) { return { success: false, message: `Falha na conexão com ${equipmentId}: ${error.message}` }; } } /** * Lista equipamentos disponíveis */ listEquipments() { const equipments = []; for (const [id, equipment] of this.equipamentos) { equipments.push({ id: id, name: equipment.name, host: equipment.host, port: equipment.port || 22 }); } return equipments; } /** * Adiciona um novo equipamento * @param {string} id - ID do equipamento * @param {Object} equipment - Dados do equipamento */ addEquipment(id, equipment) { this.equipamentos.set(id, equipment); } /** * Remove um equipamento * @param {string} id - ID do equipamento */ removeEquipment(id) { return this.equipamentos.delete(id); } /** * Lista conexões ativas */ getActiveConnections() { const connections = []; for (const [id, conn] of this.connections) { connections.push({ id: id, equipment: conn.equipment.name, host: conn.equipment.host, connectedAt: conn.connectedAt, connected: conn.connected }); } return connections; } /** * Fecha todas as conexões */ closeAllConnections() { for (const [id, connection] of this.connections) { connection.connection.end(); } this.connections.clear(); } /** * Desconecta um usuário específico no concentrador Juniper * @param {string} equipmentId - ID do equipamento Juniper * @param {string} username - Nome do usuário a ser desconectado * @returns {Promise} - Promise com resultado da operação */ async disconnectJuniperUser(equipmentId, username) { try { // Conectar ao equipamento const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } // Executar comando de desconexão const command = `clear network-access aaa subscriber username ${username}`; const result = await this.executeCommand(connection.connectionId, command); // Desconectar this.disconnect(connection.connectionId); return { success: result.success, message: result.success ? `Usuário ${username} desconectado com sucesso` : `Erro ao desconectar usuário: ${result.error}`, output: result.output, error: result.error }; } catch (error) { return { success: false, message: `Erro na operação: ${error.message}`, error: error.message }; } } /** * Lista usuários conectados no Juniper * @param {string} equipmentId - ID do equipamento Juniper * @returns {Promise} - Promise com lista de usuários */ async listJuniperUsers(equipmentId) { try { const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } // Comando para listar usuários conectados const command = 'show network-access aaa subscribers'; const result = await this.executeCommand(connection.connectionId, command); this.disconnect(connection.connectionId); return { success: result.success, message: 'Lista de usuários obtida', output: result.output, error: result.error }; } catch (error) { return { success: false, message: `Erro ao listar usuários: ${error.message}`, error: error.message }; } } /** * Busca informações de um assinante específico no concentrador Juniper * @param {string} equipmentId - ID do equipamento Juniper * @param {string} username - Login do usuário a pesquisar * @returns {Promise} - Promise com status, IP e interface PPPoE do usuário */ async getJuniperSubscriberInfo(equipmentId, username) { try { const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } const command = `show subscribers terse | find ${username}`; const result = await this.executeCommand(connection.connectionId, command); this.disconnect(connection.connectionId); if (!result.success && !result.output) { return { success: false, connected: false, message: `Erro ao consultar assinante: ${result.error}`, username }; } // Procurar linha que contenha o username const lines = result.output.split('\n'); const userLine = lines.find(l => l.includes(username)); if (!userLine) { return { success: true, connected: false, message: `Usuário ${username} não está conectado`, username, ip: null, interface: null }; } // Formato típico da saída terse: // pp0.1234 inet 203.0.113.5/32 user@dominio PPPoE // Colunas: interface tipo endereço usuário [estado] const parts = userLine.trim().split(/\s+/); const iface = parts[0] || null; // Endereço IP fica na coluna que contém "/" const ipField = parts.find(p => p.includes('/') || /^\d{1,3}(\.\d{1,3}){3}$/.test(p)); const ip = ipField ? ipField.split('/')[0] : null; return { success: true, connected: true, message: `Usuário ${username} está conectado`, username, ip, interface: iface, rawLine: userLine.trim() }; } catch (error) { return { success: false, connected: false, message: `Erro ao consultar assinante: ${error.message}`, username, error: error.message }; } } /** * Verifica o status da porta Ethernet de uma ONU Datacom pelo slot e ID * @param {string} equipmentId - ID do equipamento OLT * @param {string} slot - Slot da ONU (ex: 1/1/1) * @param {string} onuId - ID da ONU * @returns {Promise} - { success, portStatus, speed, rawOutput } */ async checkOnuEthernetDatacom(equipmentId, slot, onuId) { try { const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } const command = `show interface gpon ${slot} onu ${onuId} ethernet`; const result = await this.executeCommand(connection.connectionId, command); this.disconnect(connection.connectionId); if (!result.success) { return { success: false, error: result.error, rawOutput: result.output }; } // Extrai status e velocidade da porta ethernet // Linha típica: " 1 Forwarding 1Gbit Full-Duplex" // ou: " 1 Down N/A" const portStatus = null; const speed = null; const output = result.output || ''; // Procurar por linha de porta ethernet const lineMatch = output.match(/\b(Forwarding|Down|Up|Blocking)\b.*?([\d]+(?:Gbit|Mbit)[^\n]*|N\/A)/i); const statusVal = lineMatch ? lineMatch[1] : null; const speedVal = lineMatch ? lineMatch[2]?.trim() : null; return { success: true, portStatus: statusVal, speed: speedVal, rawOutput: output }; } catch (error) { return { success: false, error: error.message, rawOutput: null }; } } /** * Busca informações de uma ONU pelo serial number na OLT * @param {string} equipmentId - ID do equipamento OLT * @param {string} serialNumber - Serial number da ONU * @returns {Promise} - Promise com informações da ONU */ async findOnuBySerial(equipmentId, serialNumber) { try { const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } // Comando para buscar a ONU pelo serial const searchCommand = `show running-config interface gpon | context-match "${serialNumber}"`; const result = await this.executeCommand(connection.connectionId, searchCommand); this.disconnect(connection.connectionId); if (!result.success || !result.output) { return { success: false, message: `ONU com serial ${serialNumber} não encontrada`, error: result.error }; } // Extrair slot e ONU ID da saída const onuInfo = this.parseOnuInfo(result.output, serialNumber); return { success: true, message: `ONU encontrada: Slot ${onuInfo.slot}, ONU ${onuInfo.onuId}`, onuInfo: onuInfo, output: result.output }; } catch (error) { return { success: false, message: `Erro ao buscar ONU: ${error.message}`, error: error.message }; } } /** * Verifica o sinal de uma ONU específica * @param {string} equipmentId - ID do equipamento OLT * @param {string} slot - Slot da ONU (ex: 1/1/1) * @param {string} onuId - ID da ONU * @returns {Promise} - Promise com informações do sinal */ async checkOnuSignal(equipmentId, slot, onuId) { try { const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } // Comando para verificar o sinal da ONU const signalCommand = `show interface gpon ${slot} onu ${onuId} | nomore`; const result = await this.executeCommand(connection.connectionId, signalCommand); this.disconnect(connection.connectionId); return { success: result.success, message: result.success ? `Sinal da ONU ${onuId} no slot ${slot}` : `Erro ao verificar sinal: ${result.error}`, output: result.output, error: result.error, slot: slot, onuId: onuId }; } catch (error) { return { success: false, message: `Erro ao verificar sinal: ${error.message}`, error: error.message }; } } /** * Verifica o sinal de uma ONU pelo serial number (processo completo) * @param {string} equipmentId - ID do equipamento OLT * @param {string} serialNumber - Serial number da ONU * @returns {Promise} - Promise com informações completas do sinal */ async checkOnuSignalBySerial(equipmentId, serialNumber) { try { // Etapa 1: Encontrar a ONU pelo serial const findResult = await this.findOnuBySerial(equipmentId, serialNumber); if (!findResult.success) { return findResult; } // Etapa 2: Verificar o sinal usando slot e ONU ID const signalResult = await this.checkOnuSignal( equipmentId, findResult.onuInfo.slot, findResult.onuInfo.onuId ); return { success: signalResult.success, message: `Sinal da ONU ${serialNumber}`, serialNumber: serialNumber, slot: findResult.onuInfo.slot, onuId: findResult.onuInfo.onuId, signalOutput: signalResult.output, searchOutput: findResult.output, error: signalResult.error }; } catch (error) { return { success: false, message: `Erro no processo completo: ${error.message}`, error: error.message }; } } /** * Extrai informações da ONU a partir da saída do comando de busca * @param {string} output - Saída do comando show running-config * @param {string} serialNumber - Serial number procurado * @returns {Object} - Objeto com slot e onuId */ parseOnuInfo(output, serialNumber) { const lines = output.split('\n'); let currentSlot = null; let onuId = null; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Procurar por interface gpon const interfaceMatch = line.match(/interface gpon (\d+\/\d+\/\d+)/); if (interfaceMatch) { currentSlot = interfaceMatch[1]; continue; } // Procurar por onu ID const onuMatch = line.match(/onu (\d+)/); if (onuMatch && currentSlot) { // Verificar se o serial number está nas próximas linhas for (let j = i; j < Math.min(i + 5, lines.length); j++) { if (lines[j].includes(serialNumber)) { onuId = onuMatch[1]; break; } } if (onuId) break; } } return { slot: currentSlot, onuId: onuId, found: currentSlot && onuId }; } /** * Lista todos os equipamentos OLT disponíveis * @returns {Array} - Array com OLTs disponíveis */ listOLTs() { const olts = []; for (const [id, equipment] of this.equipamentos) { if (equipment.type === 'olt') { olts.push({ id: id, name: equipment.name, host: equipment.host, port: equipment.port || 22 }); } } return olts; } /** * Reinicia uma ONU específica * @param {string} equipmentId - ID do equipamento OLT * @param {string} slot - Slot da ONU (ex: 1/1/1) * @param {string} onuId - ID da ONU * @returns {Promise} - Promise com resultado da operação */ async resetOnu(equipmentId, slot, onuId) { try { const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } // Executar comandos em sequência const commands = [ 'config', `interface gpon ${slot}`, `onu-reset onu ${onuId}`, 'yes', 'exit' ]; let fullOutput = ''; for (let i = 0; i < commands.length; i++) { const command = commands[i]; try { // Para o comando 'yes', usar um delay menor if (command === 'yes') { await new Promise(resolve => setTimeout(resolve, 2000)); } const result = await this.executeCommand(connection.connectionId, command); fullOutput += `Command: ${command}\n${result.output}\n\n`; // Verificar se houve erro específico if (result.error && !result.error.includes('yes') && command !== 'yes') { console.warn(`Aviso no comando ${command}: ${result.error}`); } // Delay entre comandos para permitir processamento if (i < commands.length - 1) { await new Promise(resolve => setTimeout(resolve, 1500)); } } catch (cmdError) { console.warn(`Erro no comando ${command}: ${cmdError.message}`); if (command !== 'yes' && command !== 'exit') { // Se não for yes ou exit, é um erro crítico throw cmdError; } } } // Desconectar this.disconnect(connection.connectionId); return { success: true, message: `ONU ${onuId} no slot ${slot} foi reiniciada com sucesso`, output: fullOutput.trim(), slot: slot, onuId: onuId }; } catch (error) { return { success: false, message: `Erro ao reiniciar ONU: ${error.message}`, error: error.message }; } } /** * Reinicia uma ONU específica usando shell interativo * @param {string} equipmentId - ID do equipamento OLT * @param {string} slot - Slot da ONU (ex: 1/1/1) * @param {string} onuId - ID da ONU * @returns {Promise} - Promise com resultado da operação */ async resetOnuAlternative(equipmentId, slot, onuId) { console.log(`🔄 Iniciando reset ONU - OLT: ${equipmentId}, Slot: ${slot}, ONU: ${onuId}`); return new Promise(async (resolve, reject) => { try { console.log(`📡 Conectando na OLT: ${equipmentId}`); const connection = await this.connect(equipmentId); if (!connection.success) { console.log(`❌ Falha na conexão: ${connection.message}`); throw new Error(`Falha na conexão: ${connection.message}`); } console.log(`✅ Conectado! ConnectionId: ${connection.connectionId}`); const conn = this.connections.get(connection.connectionId); // Usar shell interativo para manter contexto conn.connection.shell((err, stream) => { if (err) { this.disconnect(connection.connectionId); reject(new Error(`Erro ao abrir shell: ${err.message}`)); return; } let output = ''; let step = 0; let isComplete = false; const commands = [ 'config', `interface gpon ${slot}`, `onu-reset onu ${onuId}`, 'yes', 'exit', // Sai do modo interface 'exit', // Sai do modo configuração 'exit' // Encerra a sessão SSH ]; stream.on('data', (data) => { const dataStr = data.toString(); output += dataStr; console.log(`� Recebido: ${dataStr.trim()}`); // Detectar prompts e enviar próximo comando if (step < commands.length) { let shouldSendCommand = false; if (step === 0 && dataStr.includes('#')) { // Prompt inicial - enviar config shouldSendCommand = true; } else if (step === 1 && dataStr.includes('(config)#')) { // No modo config - enviar interface shouldSendCommand = true; } else if (step === 2 && dataStr.includes('(config-gpon')) { // No modo interface - enviar onu-reset shouldSendCommand = true; } else if (step === 3 && (dataStr.includes('Are you sure') || dataStr.includes('[no,yes]'))) { // Pergunta de confirmação - enviar yes shouldSendCommand = true; } else if (step === 4 && dataStr.includes('(config-gpon')) { // Após confirmação - enviar primeiro exit shouldSendCommand = true; } else if (step === 5 && dataStr.includes('(config)#')) { // De volta ao config - enviar segundo exit shouldSendCommand = true; } else if (step === 6 && dataStr.includes('#') && !dataStr.includes('(config')) { // Voltou ao prompt raiz - enviar exit final para encerrar sessão shouldSendCommand = true; } if (shouldSendCommand) { console.log(`📝 Enviando comando ${step + 1}: ${commands[step]}`); stream.write(commands[step] + '\n'); step++; } } else if (!isComplete) { // Todos os comandos enviados - aguardar fechamento do stream isComplete = true; setTimeout(() => { stream.end(); }, 1000); } }); stream.on('close', () => { this.disconnect(connection.connectionId); console.log(`🔌 Desconectado da OLT`); resolve({ success: true, message: `ONU ${onuId} no slot ${slot} foi reiniciada com sucesso`, output: output.trim(), slot: slot, onuId: onuId }); }); stream.on('error', (err) => { this.disconnect(connection.connectionId); console.error(`❌ Erro no shell: ${err.message}`); reject(new Error(`Erro no shell: ${err.message}`)); }); // Timeout de segurança (60 segundos) setTimeout(() => { if (!stream.destroyed && !isComplete) { console.log(`⏰ Timeout - forçando fechamento`); stream.end(); this.disconnect(connection.connectionId); reject(new Error('Timeout na operação de reset')); } }, 60000); }); } catch (error) { console.error(`❌ Erro ao reiniciar ONU:`, error); reject(new Error(`Erro ao reiniciar ONU: ${error.message}`)); } }); } /** * Obtém informações detalhadas da ONU incluindo Equipment ID * @param {string} equipmentId - ID do equipamento OLT * @param {string} slot - Slot da ONU (ex: 1/1/1) * @param {string} onuId - ID da ONU * @returns {Promise} - Promise com informações da ONU */ async getOnuDetails(equipmentId, slot, onuId) { try { console.log(`🔍 Obtendo detalhes da ONU - OLT: ${equipmentId}, Slot: ${slot}, ONU: ${onuId}`); const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } // Comando para obter informações detalhadas da ONU const detailsCommand = `show interface gpon ${slot} onu ${onuId}`; const result = await this.executeCommand(connection.connectionId, detailsCommand); this.disconnect(connection.connectionId); if (!result.success) { return { success: false, message: `Erro ao obter detalhes da ONU: ${result.error}` }; } // Extrair Equipment ID da saída const equipmentId_onu = this.parseEquipmentId(result.output); return { success: true, message: `Detalhes da ONU obtidos com sucesso`, equipmentId: equipmentId_onu, slot: slot, onuId: onuId, output: result.output }; } catch (error) { console.error(`❌ Erro ao obter detalhes da ONU:`, error); return { success: false, message: `Erro ao obter detalhes: ${error.message}`, error: error.message }; } } /** * Extrai o Equipment ID da saída do comando show interface * @param {string} output - Saída do comando * @returns {string} - Equipment ID identificado */ parseEquipmentId(output) { const lines = output.split('\n'); for (const line of lines) { // Procurar por "Equipment ID" na saída if (line.includes('Equipment ID') || line.includes('equipment id')) { // Extrair o valor após "Equipment ID:" const match = line.match(/Equipment\s+ID\s*:\s*([^\s]+)/i); if (match) { return match[1].trim(); } } // Formato alternativo if (line.includes('DM986-100') || line.includes('DM986-414')) { const match = line.match(/(DM986-\d+)/); if (match) { return match[1]; } } } return 'UNKNOWN'; } /** * Atualiza firmware da ONU * @param {string} equipmentId - ID do equipamento OLT * @param {string} slot - Slot da ONU (ex: 1/1/1) * @param {string} onuId - ID da ONU * @param {string} firmwareFile - Nome do arquivo de firmware * @returns {Promise} - Promise com resultado da operação */ async updateOnuFirmware(equipmentId, slot, onuId, firmwareFile) { console.log(`🔄 Iniciando update firmware ONU - OLT: ${equipmentId}, Slot: ${slot}, ONU: ${onuId}, FW: ${firmwareFile}`); try { console.log(`📡 Conectando na OLT: ${equipmentId}`); const connection = await this.connect(equipmentId); if (!connection.success) { console.log(`❌ Falha na conexão: ${connection.message}`); throw new Error(`Falha na conexão: ${connection.message}`); } console.log(`✅ Conectado! ConnectionId: ${connection.connectionId}`); // Comando de update firmware const firmwareCommand = `request firmware onu install ${firmwareFile} interface gpon ${slot} onu ${onuId}`; console.log(`📝 Enviando comando: ${firmwareCommand}`); // Enviar comando e considerar sucesso imediatamente após envio const result = await this.executeCommand(connection.connectionId, firmwareCommand); // Desconectar this.disconnect(connection.connectionId); console.log(`🔌 Desconectado da OLT`); // Considerar sucesso se o comando foi enviado (mesmo que ainda esteja processando) return { success: true, message: `Comando de atualização de firmware enviado com sucesso`, firmwareFile: firmwareFile, slot: slot, onuId: onuId, command: firmwareCommand, output: result.output || 'Comando enviado para processamento' }; } catch (error) { console.error(`❌ Erro ao atualizar firmware:`, error); return { success: false, message: `Erro ao enviar comando de atualização: ${error.message}`, error: error.message }; } } /** * Encontra o service-port de uma ONU específica * @param {string} equipmentId - ID do equipamento OLT * @param {string} slot - Slot da ONU (ex: 1/1/1) * @param {string} onuId - ID da ONU * @returns {Promise} - Promise com service-port encontrado */ async findServicePort(equipmentId, slot, onuId) { try { console.log(`🔍 Procurando service-port - OLT: ${equipmentId}, Slot: ${slot}, ONU: ${onuId}`); const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } // Comando para encontrar service-port const searchCommand = `show running-config service-port | context-match "gpon ${slot} onu ${onuId}"`; console.log(`📝 Executando comando: ${searchCommand}`); const result = await this.executeCommand(connection.connectionId, searchCommand); this.disconnect(connection.connectionId); if (!result.success) { return { success: false, message: `Erro ao buscar service-port: ${result.error}` }; } // Extrair service-ports que pertencem EXATAMENTE à ONU (match exato, não substring) const servicePorts = this.parseServicePortsForOnu(result.output, slot, onuId); if (servicePorts.length === 0) { return { success: false, message: `Service-port não encontrado para ONU ${onuId} no slot ${slot}`, output: result.output }; } return { success: true, message: `Service-port(s) encontrado(s): ${servicePorts.join(', ')}`, servicePort: servicePorts[0], // compatibilidade retroativa servicePorts: servicePorts, slot: slot, onuId: onuId, output: result.output }; } catch (error) { console.error(`❌ Erro ao buscar service-port:`, error); return { success: false, message: `Erro ao buscar service-port: ${error.message}`, error: error.message }; } } /** * Extrai o service-port da saída do comando (retorna apenas o primeiro) * @param {string} output - Saída do comando show running-config service-port * @returns {string|null} - Service-port encontrado ou null */ parseServicePort(output) { const ports = this.parseServicePorts(output); return ports.length > 0 ? ports[0] : null; } /** * Extrai todos os service-ports da saída do comando * @param {string} output - Saída do comando show running-config service-port * @returns {string[]} - Array com todos os service-ports encontrados */ parseServicePorts(output) { const lines = output.split('\n'); const ports = []; for (const line of lines) { const match = line.match(/service-port\s+(\d+)/); if (match) { const port = match[1]; if (!ports.includes(port)) { ports.push(port); } } } return ports; } /** * Extrai service-ports que pertencem EXATAMENTE à ONU indicada. * O context-match faz substring match, então "onu 3" bate em "onu 30", "onu 31", etc. * Este método verifica a linha de configuração de cada bloco para garantir match exato. * @param {string} output - Saída do comando show running-config service-port * @param {string} slot - Slot da ONU (ex: 1/1/9) * @param {string|number} onuId - ID exato da ONU * @returns {string[]} - Array com os service-ports que pertencem exclusivamente à ONU */ parseServicePortsForOnu(output, slot, onuId) { const escapedSlot = slot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escapedOnuId = String(onuId).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Garante que "onu 3" não case com "onu 30": exige espaço ou fim de linha após o ID const onuPattern = new RegExp(`gpon\\s+${escapedSlot}\\s+onu\\s+${escapedOnuId}(?:\\s|$)`); const lines = output.split('\n'); const ports = []; let currentPort = null; for (const line of lines) { const portMatch = line.match(/^service-port\s+(\d+)/); if (portMatch) { currentPort = portMatch[1]; } else if (currentPort !== null) { if (onuPattern.test(line)) { if (!ports.includes(currentPort)) { ports.push(currentPort); } } // Cada bloco tem apenas uma linha de configuração; reseta após verificar currentPort = null; } } return ports; } /** * Remove uma ONU específica (processo completo) * Busca automaticamente todos os service-ports e remove cada um deles. * @param {string} equipmentId - ID do equipamento OLT * @param {string} slot - Slot da ONU (ex: 1/1/1) * @param {string} onuId - ID da ONU * @returns {Promise} - Promise com resultado da operação */ async removeOnu(equipmentId, slot, onuId) { console.log(`🗑️ Iniciando remoção de ONU - OLT: ${equipmentId}, Slot: ${slot}, ONU: ${onuId}`); return new Promise(async (resolve, reject) => { try { console.log(`📡 Conectando na OLT: ${equipmentId}`); const connection = await this.connect(equipmentId); if (!connection.success) { console.log(`❌ Falha na conexão: ${connection.message}`); throw new Error(`Falha na conexão: ${connection.message}`); } console.log(`✅ Conectado! ConnectionId: ${connection.connectionId}`); // Buscar todos os service-ports antes de abrir o shell interativo const searchCommand = `show running-config service-port | context-match "gpon ${slot} onu ${onuId}"`; console.log(`🔍 Buscando service-ports: ${searchCommand}`); const spResult = await this.executeCommand(connection.connectionId, searchCommand); // Match exato: "onu 3" não pode casar com "onu 30", "onu 31", etc. const servicePorts = spResult.success ? this.parseServicePortsForOnu(spResult.output, slot, onuId) : []; if (servicePorts.length === 0) { this.disconnect(connection.connectionId); return resolve({ success: false, message: `Nenhum service-port encontrado para ONU ${onuId} no slot ${slot}`, slot: slot, onuId: onuId }); } console.log(`🔌 Service-port(s) encontrado(s): ${servicePorts.join(', ')}`); const conn = this.connections.get(connection.connectionId); // Usar shell interativo para manter contexto conn.connection.shell((err, stream) => { if (err) { this.disconnect(connection.connectionId); reject(new Error(`Erro ao abrir shell: ${err.message}`)); return; } let output = ''; let step = 0; let isComplete = false; // Construir lista de comandos dinamicamente para N service-ports // 0: config // 1: interface gpon // 2: no onu // 3: top // 4..4+n-1: no service-port (um por service-port) // 4+n: commit // 4+n+1: top // 4+n+2: exit // 4+n+3: exit const commands = [ 'config', `interface gpon ${slot}`, `no onu ${onuId}`, 'top', ...servicePorts.map(sp => `no service-port ${sp}`), 'commit', 'top', 'exit', 'exit' ]; // Índice do comando commit (usado como referência para os demais) const commitIdx = 4 + servicePorts.length; stream.on('data', (data) => { const dataStr = data.toString(); output += dataStr; console.log(`📨 Recebido: ${dataStr.trim()}`); if (step < commands.length) { let shouldSendCommand = false; if (step === 0 && dataStr.includes('#')) { // Prompt inicial - enviar config shouldSendCommand = true; } else if (step === 1 && dataStr.includes('(config)#')) { // No modo config - enviar interface shouldSendCommand = true; } else if (step === 2 && dataStr.includes('(config-gpon')) { // No modo interface gpon - enviar no onu shouldSendCommand = true; } else if (step === 3 && dataStr.includes('(config-gpon')) { // Após no onu - enviar top shouldSendCommand = true; } else if (step >= 4 && step <= commitIdx + 2 && dataStr.includes('(config)#')) { // Remove todos os service-ports + commit + top + primeiro exit shouldSendCommand = true; } else if (step === commitIdx + 3 && dataStr.includes('#') && !dataStr.includes('(config')) { // Último exit shouldSendCommand = true; } if (shouldSendCommand) { console.log(`📝 Enviando comando ${step + 1}/${commands.length}: ${commands[step]}`); stream.write(commands[step] + '\n'); step++; } } else if (!isComplete && dataStr.includes('#') && !dataStr.includes('(config')) { // Voltou ao prompt normal - operação completa isComplete = true; setTimeout(() => { stream.end(); }, 1000); } }); stream.on('close', () => { this.disconnect(connection.connectionId); console.log(`🔌 Desconectado da OLT`); resolve({ success: true, message: `ONU ${onuId} removida com sucesso do slot ${slot}`, output: output.trim(), slot: slot, onuId: onuId, servicePorts: servicePorts }); }); stream.on('error', (err) => { this.disconnect(connection.connectionId); console.error(`❌ Erro no shell: ${err.message}`); reject(new Error(`Erro no shell: ${err.message}`)); }); // Timeout de segurança (60 segundos) setTimeout(() => { if (!stream.destroyed && !isComplete) { console.log(`⏰ Timeout - forçando fechamento`); stream.end(); this.disconnect(connection.connectionId); reject(new Error('Timeout na operação de remoção')); } }, 60000); }); } catch (error) { console.error(`❌ Erro ao remover ONU:`, error); reject(new Error(`Erro ao remover ONU: ${error.message}`)); } }); } /** * Processo completo de remoção de ONU pelo serial number * @param {string} equipmentId - ID do equipamento OLT * @param {string} serialNumber - Serial number da ONU * @returns {Promise} - Promise com resultado da operação completa */ async removeOnuBySerial(equipmentId, serialNumber) { try { console.log(`🗑️ Iniciando processo de remoção - Serial: ${serialNumber}`); // Etapa 1: Encontrar a ONU pelo serial const findResult = await this.findOnuBySerial(equipmentId, serialNumber); if (!findResult.success) { return { success: false, message: `ONU com serial ${serialNumber} não encontrada`, error: findResult.message }; } // Etapa 2: Encontrar o service-port const servicePortResult = await this.findServicePort( equipmentId, findResult.onuInfo.slot, findResult.onuInfo.onuId ); if (!servicePortResult.success) { return { success: false, message: `Service-port não encontrado para ONU ${serialNumber}`, error: servicePortResult.message, onuInfo: findResult.onuInfo }; } // Retornar informações para confirmação return { success: true, message: `ONU encontrada e pronta para remoção`, serialNumber: serialNumber, slot: findResult.onuInfo.slot, onuId: findResult.onuInfo.onuId, servicePort: servicePortResult.servicePorts[0], // compatibilidade retroativa servicePorts: servicePortResult.servicePorts, findOutput: findResult.output, servicePortOutput: servicePortResult.output, readyForRemoval: true }; } catch (error) { return { success: false, message: `Erro no processo de remoção: ${error.message}`, error: error.message }; } } /** * Mapas de VLAN por OLT e slot */ getVlanMapping() { return { 'olt_dito': { '1/1/1': 601, '1/1/2': 602, '1/1/3': 603, '1/1/4': 604 }, 'olt_colmeia': { '1/1/1': 605, '1/1/2': 606, '1/1/3': 607, '1/1/4': 608, '1/1/5': 609, '1/1/6': 610, '1/1/7': 611, '1/1/8': 612, '1/1/9': 613, '1/1/10': 614, '1/1/11': 608, '1/1/12': 616, '1/1/13': 617, '1/1/14': 618, '1/1/15': 619, '1/1/16': 620 }, 'olt_canaa': { '1/1/1': 605, '1/1/2': 606, '1/1/3': 607, '1/1/4': 608, '1/1/5': 609, '1/1/6': 610, '1/1/7': 611, '1/1/8': 612 }, 'olt_canaa2': { '1/1/1': 621, '1/1/2': 622, '1/1/3': 623, '1/1/4': 624, '1/1/5': 625, '1/1/6': 626, '1/1/7': 627, '1/1/8': 628 }, 'olt_pedro_afonso': { '1/1/1': 301, '1/1/2': 302, '1/1/3': 303, '1/1/4': 304, '1/1/5': 305, '1/1/6': 306, '1/1/7': 307, '1/1/8': 308, '1/1/9': 309, '1/1/10': 310, '1/1/11': 311, '1/1/12': 312, '1/1/13': 313, '1/1/14': 314, '1/1/15': 315, '1/1/16': 316 }, 'olt_canavieiras': { '1/1/1': 601, '1/1/2': 602, '1/1/3': 603, '1/1/4': 604, '1/1/5': 605, '1/1/6': 606, '1/1/7': 607, '1/1/8': 608, '1/1/9': 609, '1/1/10': 610, '1/1/11': 611, '1/1/12': 612, '1/1/13': 613, '1/1/14': 614, '1/1/15': 615, '1/1/16': 616 } }; } /** * Descobre o slot de uma ONU pelo serial na lista de discovered-onus * @param {string} equipmentId - ID do equipamento OLT * @param {string} serialNumber - Serial da ONU * @returns {Promise} - Promise com slot encontrado */ async discoverOnuSlot(equipmentId, serialNumber) { try { console.log(`🔍 Descobrindo slot da ONU ${serialNumber} na OLT ${equipmentId}`); const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } const result = await this.executeCommand(connection.connectionId, 'show interface gpon discovered-onus'); this.disconnect(connection.connectionId); if (!result.success) { return { success: false, message: `Erro ao buscar discovered-onus: ${result.error}` }; } // Procurar o serial na saída const lines = result.output.split('\n'); for (const line of lines) { if (line.includes(serialNumber)) { // Formato: " 1/1/3 DACM915E0F76" const match = line.match(/\s+(\d+\/\d+\/\d+)\s+([A-Z0-9]+)/); if (match && match[2] === serialNumber) { return { success: true, slot: match[1], serialNumber: serialNumber, output: result.output }; } } } return { success: false, message: `ONU ${serialNumber} não encontrada na lista de discovered-onus`, output: result.output }; } catch (error) { console.error(`❌ Erro ao descobrir slot:`, error); return { success: false, message: `Erro ao descobrir slot: ${error.message}`, error: error.message }; } } /** * Descobre ONU IDs já em uso em um slot específico * @param {string} equipmentId - ID do equipamento OLT * @param {string} slot - Slot a verificar (ex: 1/1/1) * @returns {Promise} - Promise com lista de IDs em uso */ async getUsedOnuIds(equipmentId, slot) { try { console.log(`🔍 Buscando ONU IDs em uso no slot ${slot} da OLT ${equipmentId}`); const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } const result = await this.executeCommand(connection.connectionId, `show interface gpon ${slot} onu | nomore`); this.disconnect(connection.connectionId); if (!result.success) { return { success: false, message: `Erro ao buscar ONUs do slot: ${result.error}` }; } // Extrair ONU IDs da saída const usedIds = new Set(); const lines = result.output.split('\n'); for (const line of lines) { // Formato: "1/1/1 0 DACM913D21A0 Up None" const match = line.match(/\d+\/\d+\/\d+\s+(\d+)\s+[A-Z0-9]+/); if (match) { usedIds.add(parseInt(match[1])); } } return { success: true, usedIds: Array.from(usedIds).sort((a, b) => a - b), output: result.output }; } catch (error) { console.error(`❌ Erro ao buscar ONU IDs:`, error); return { success: false, message: `Erro ao buscar ONU IDs: ${error.message}`, error: error.message }; } } /** * Encontra o próximo ONU ID disponível (0-127) * @param {Array} usedIds - Array de IDs já em uso * @returns {number|null} - Próximo ID disponível ou null se todos estão em uso */ findNextAvailableOnuId(usedIds) { for (let id = 0; id <= 127; id++) { if (!usedIds.includes(id)) { return id; } } return null; // Todos os IDs estão em uso } /** * Adiciona uma nova ONU ao sistema * @param {string} equipmentId - ID do equipamento OLT * @param {string} slot - Slot da ONU (ex: 1/1/1) * @param {string} onuId - ID da ONU (0-127) * @param {string} serialNumber - Serial da ONU * @param {number} vlanId - VLAN ID para a ONU * @returns {Promise} - Promise com resultado da operação */ async addOnu(equipmentId, slot, onuId, serialNumber, vlanId) { console.log(`➕ Adicionando ONU - OLT: ${equipmentId}, Slot: ${slot}, ONU ID: ${onuId}, Serial: ${serialNumber}, VLAN: ${vlanId}`); return new Promise(async (resolve, reject) => { try { console.log(`📡 Conectando na OLT: ${equipmentId}`); const connection = await this.connect(equipmentId); if (!connection.success) { console.log(`❌ Falha na conexão: ${connection.message}`); throw new Error(`Falha na conexão: ${connection.message}`); } console.log(`✅ Conectado! ConnectionId: ${connection.connectionId}`); const conn = this.connections.get(connection.connectionId); // Usar shell interativo para manter contexto conn.connection.shell((err, stream) => { if (err) { this.disconnect(connection.connectionId); reject(new Error(`Erro ao abrir shell: ${err.message}`)); return; } let output = ''; let step = 0; let isComplete = false; let servicePortId = null; const commands = [ 'config', `interface gpon ${slot}`, `onu ${onuId}`, 'line-profile DM986-100', `serial-number ${serialNumber}`, 'ethernet 1', `native vlan vlan-id ${vlanId}`, 'top', 'service-port new', `gpon ${slot} onu ${onuId} gem 1 match vlan vlan-id ${vlanId} action vlan replace vlan-id ${vlanId}`, 'commit', 'top', 'exit', 'exit' ]; stream.on('data', (data) => { const dataStr = data.toString(); output += dataStr; console.log(`📨 Recebido: ${dataStr.trim()}`); // Capturar service-port ID APENAS do prompt de configuração após 'service-port new' // Isso garante que pegamos o ID correto da ONU que estamos adicionando if (dataStr.includes('(config-service-port-') && /\(config-service-port-(\d+)\)/.test(dataStr)) { const match = dataStr.match(/\(config-service-port-(\d+)\)/); if (match && !servicePortId) { servicePortId = match[1]; console.log(`✅ Service-port ID capturado do prompt: ${servicePortId}`); } } // Detectar prompts e enviar próximo comando if (step < commands.length) { let shouldSendCommand = false; if (step === 0 && dataStr.includes('#')) { // Prompt inicial - enviar config shouldSendCommand = true; } else if (step === 1 && dataStr.includes('(config)#')) { // No modo config - enviar interface shouldSendCommand = true; } else if (step === 2 && dataStr.includes('(config-gpon')) { // No modo interface - enviar onu shouldSendCommand = true; } else if (step >= 3 && step <= 5 && dataStr.includes('(config-gpon-onu')) { // No modo ONU - enviar próximos comandos (line-profile, serial-number, ethernet) shouldSendCommand = true; } else if (step === 6 && dataStr.includes('(config-ethernet')) { // No modo ethernet - enviar native vlan shouldSendCommand = true; } else if (step === 7 && dataStr.includes('(config-ethernet')) { // Após native vlan - enviar top para voltar ao config shouldSendCommand = true; } else if (step === 8 && dataStr.includes('(config)#')) { // Após top - enviar service-port new shouldSendCommand = true; } else if (step === 9 && dataStr.includes('(config-service-port')) { // Após service-port new - enviar configuração service-port (agora em modo service-port) shouldSendCommand = true; } else if (step === 10 && dataStr.includes('(config-service-port')) { // No modo service-port - enviar commit shouldSendCommand = true; } else if (step === 11 && dataStr.includes('(config-service-port')) { // Após commit (ainda em service-port) - enviar top shouldSendCommand = true; } else if (step === 12 && dataStr.includes('(config)#')) { // Após top - enviar exit shouldSendCommand = true; } else if (step === 13 && dataStr.includes('#') && !dataStr.includes('(config')) { // Último exit - finalizar shouldSendCommand = true; } if (shouldSendCommand) { console.log(`📝 Enviando comando ${step + 1}: ${commands[step]}`); stream.write(commands[step] + '\n'); step++; } } else if (!isComplete && dataStr.includes('#') && !dataStr.includes('(config')) { // Voltou ao prompt normal - operação completa isComplete = true; setTimeout(() => { stream.end(); }, 1000); } }); stream.on('close', () => { this.disconnect(connection.connectionId); console.log(`🔌 Desconectado da OLT`); resolve({ success: true, message: `ONU ${serialNumber} adicionada com sucesso`, output: output.trim(), slot: slot, onuId: onuId, serialNumber: serialNumber, vlanId: vlanId, servicePortId: servicePortId }); }); stream.on('error', (err) => { this.disconnect(connection.connectionId); console.error(`❌ Erro no shell: ${err.message}`); reject(new Error(`Erro no shell: ${err.message}`)); }); // Timeout de segurança (90 segundos) setTimeout(() => { if (!stream.destroyed && !isComplete) { console.log(`⏰ Timeout - forçando fechamento`); stream.end(); this.disconnect(connection.connectionId); reject(new Error('Timeout na operação de adição')); } }, 90000); }); } catch (error) { console.error(`❌ Erro ao adicionar ONU:`, error); reject(new Error(`Erro ao adicionar ONU: ${error.message}`)); } }); } /** * Processo completo de adição de ONU pelo serial number * @param {string} equipmentId - ID do equipamento OLT * @param {string} serialNumber - Serial number da ONU * @returns {Promise} - Promise com resultado da operação completa */ async addOnuBySerial(equipmentId, serialNumber) { try { console.log(`➕ Iniciando processo de adição - Serial: ${serialNumber}`); // Etapa 1: Descobrir o slot da ONU const discoverResult = await this.discoverOnuSlot(equipmentId, serialNumber); if (!discoverResult.success) { return { success: false, message: `ONU ${serialNumber} não encontrada na lista de discovered-onus`, error: discoverResult.message }; } const slot = discoverResult.slot; console.log(`✅ Slot descoberto: ${slot}`); // Etapa 2: Buscar ONU IDs já em uso const usedIdsResult = await this.getUsedOnuIds(equipmentId, slot); if (!usedIdsResult.success) { return { success: false, message: `Erro ao buscar ONU IDs em uso no slot ${slot}`, error: usedIdsResult.message }; } // Etapa 3: Encontrar próximo ID disponível const nextOnuId = this.findNextAvailableOnuId(usedIdsResult.usedIds); if (nextOnuId === null) { return { success: false, message: `Todos os ONU IDs (0-127) estão em uso no slot ${slot}`, usedIds: usedIdsResult.usedIds }; } console.log(`✅ Próximo ONU ID disponível: ${nextOnuId}`); // Etapa 4: Determinar VLAN ID baseada na OLT e slot const vlanMapping = this.getVlanMapping(); const vlanId = vlanMapping[equipmentId] && vlanMapping[equipmentId][slot]; if (!vlanId) { return { success: false, message: `VLAN não configurada para OLT ${equipmentId} slot ${slot}`, slot: slot, onuId: nextOnuId }; } console.log(`✅ VLAN ID determinada: ${vlanId}`); // Retornar informações para confirmação return { success: true, message: `ONU pronta para adição`, serialNumber: serialNumber, slot: slot, onuId: nextOnuId, vlanId: vlanId, usedIds: usedIdsResult.usedIds, readyForAddition: true }; } catch (error) { return { success: false, message: `Erro no processo de adição: ${error.message}`, error: error.message }; } } /** * Configura IPoE em ONU Datacom: altera VLAN da ethernet e service-ports para 977 * 1. Busca service-ports existentes * 2. Busca a ONU (slot/onuId) pelo serial * 3. Remove service-ports antigos * 4. Altera ethernet native vlan para 977 * 5. Cria novos service-ports com VLAN 977 * 6. Commit * @param {string} equipmentId - ID do equipamento OLT (ex: olt_dito) * @param {string} slot - Slot da ONU (ex: 1/1/1) * @param {string} onuId - ID da ONU (ex: 5) * @returns {Promise} - Promise com resultado da operação */ async configIPoEDatacom(equipmentId, slot, onuId) { console.log(`🔗 [IPoE Datacom] Iniciando - OLT: ${equipmentId}, Slot: ${slot}, ONU: ${onuId}`); return new Promise(async (resolve, reject) => { try { const connection = await this.connect(equipmentId); if (!connection.success) { throw new Error(`Falha na conexão: ${connection.message}`); } // Buscar service-ports existentes const spCommand = `show running-config service-port | context-match "gpon ${slot} onu ${onuId}"`; const spResult = await this.executeCommand(connection.connectionId, spCommand); // Match exato: "onu 3" não pode casar com "onu 30", "onu 31", etc. const servicePorts = spResult.success ? this.parseServicePortsForOnu(spResult.output, slot, onuId) : []; if (servicePorts.length === 0) { this.disconnect(connection.connectionId); return resolve({ success: false, message: `Nenhum service-port encontrado para ONU ${onuId} no slot ${slot}` }); } console.log(`🔌 [IPoE Datacom] Service-ports encontrados: ${servicePorts.join(', ')}`); const conn = this.connections.get(connection.connectionId); const VLAN_IPOE = 977; conn.connection.shell((err, stream) => { if (err) { this.disconnect(connection.connectionId); reject(new Error(`Erro ao abrir shell: ${err.message}`)); return; } let output = ''; let step = 0; let isComplete = false; let newServicePortId = null; // Sequência de comandos: // 0: config // 1..N: no service-port X (remover antigos) // N+1: interface gpon {slot} // N+2: onu {onuId} // N+3: ethernet 1 // N+4: native vlan vlan-id 977 // N+5: top // N+6: service-port new // N+7: gpon {slot} onu {onuId} gem 1 match vlan vlan-id 977 action vlan replace vlan-id 977 // N+8: commit // N+9: top // N+10: exit // N+11: exit const commands = [ 'config', ...servicePorts.map(sp => `no service-port ${sp}`), `interface gpon ${slot}`, `onu ${onuId}`, 'ethernet 1', `native vlan vlan-id ${VLAN_IPOE}`, 'top', 'service-port new', `gpon ${slot} onu ${onuId} gem 1 match vlan vlan-id ${VLAN_IPOE} action vlan replace vlan-id ${VLAN_IPOE}`, 'commit', 'top', 'exit', 'exit' ]; const removeCount = servicePorts.length; const ifaceIdx = 1 + removeCount; // interface gpon const onuIdx = ifaceIdx + 1; // onu const ethIdx = onuIdx + 1; // ethernet 1 const vlanIdx = ethIdx + 1; // native vlan const topAfterVlanIdx = vlanIdx + 1; // top (back to config) const spNewIdx = topAfterVlanIdx + 1; // service-port new const spCfgIdx = spNewIdx + 1; // gpon ... match vlan const commitIdx = spCfgIdx + 1; // commit const topAfterCommitIdx = commitIdx + 1; // top const exit1Idx = topAfterCommitIdx + 1; // exit const exit2Idx = exit1Idx + 1; // exit stream.on('data', (data) => { const dataStr = data.toString(); output += dataStr; console.log(`📨 [IPoE] Recebido: ${dataStr.trim()}`); // Capturar novo service-port ID if (dataStr.includes('(config-service-port-')) { const match = dataStr.match(/\(config-service-port-(\d+)\)/); if (match && !newServicePortId) { newServicePortId = match[1]; console.log(`✅ [IPoE] Novo service-port ID: ${newServicePortId}`); } } if (step < commands.length) { let shouldSend = false; if (step === 0 && dataStr.includes('#')) { shouldSend = true; // prompt inicial → config } else if (step >= 1 && step <= removeCount && dataStr.includes('(config)#')) { shouldSend = true; // modo config → no service-port X } else if (step === ifaceIdx && dataStr.includes('(config)#')) { shouldSend = true; // modo config → interface gpon } else if (step === onuIdx && dataStr.includes('(config-gpon')) { shouldSend = true; // modo gpon → onu } else if (step === ethIdx && dataStr.includes('(config-gpon-onu')) { shouldSend = true; // modo onu → ethernet 1 } else if (step === vlanIdx && dataStr.includes('(config-ethernet')) { shouldSend = true; // modo ethernet → native vlan } else if (step === topAfterVlanIdx && dataStr.includes('(config-ethernet')) { shouldSend = true; // após vlan → top } else if (step === spNewIdx && dataStr.includes('(config)#')) { shouldSend = true; // config → service-port new } else if (step === spCfgIdx && dataStr.includes('(config-service-port')) { shouldSend = true; // service-port → gpon ... match vlan } else if (step === commitIdx && dataStr.includes('(config-service-port')) { shouldSend = true; // service-port → commit } else if (step === topAfterCommitIdx && dataStr.includes('(config-service-port')) { shouldSend = true; // service-port → top } else if (step === exit1Idx && dataStr.includes('(config)#')) { shouldSend = true; // config → exit } else if (step === exit2Idx && dataStr.includes('#') && !dataStr.includes('(config')) { shouldSend = true; // prompt → exit } if (shouldSend) { console.log(`📝 [IPoE] Comando ${step + 1}/${commands.length}: ${commands[step]}`); stream.write(commands[step] + '\n'); step++; } } else if (!isComplete && dataStr.includes('#') && !dataStr.includes('(config')) { isComplete = true; setTimeout(() => { stream.end(); }, 1000); } }); stream.on('close', () => { this.disconnect(connection.connectionId); console.log(`✅ [IPoE Datacom] Concluído - ONU ${onuId} no slot ${slot}`); resolve({ success: true, message: `VLAN alterada para ${VLAN_IPOE} (IPoE)`, output: output.trim(), slot, onuId, vlanId: VLAN_IPOE, oldServicePorts: servicePorts, newServicePortId }); }); stream.on('error', (err) => { this.disconnect(connection.connectionId); reject(new Error(`Erro no shell: ${err.message}`)); }); setTimeout(() => { if (!stream.destroyed && !isComplete) { console.log(`⏰ [IPoE Datacom] Timeout`); stream.end(); this.disconnect(connection.connectionId); reject(new Error('Timeout na operação IPoE')); } }, 90000); }); } catch (error) { console.error(`❌ [IPoE Datacom] Erro:`, error); reject(new Error(`Erro IPoE Datacom: ${error.message}`)); } }); } } module.exports = SSHManager;