Files
rodrigonet/src/ssh/manager.js
T
2026-04-15 10:57:48 -03:00

2071 lines
82 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <slot>
// 2: no onu <onuId>
// 3: top
// 4..4+n-1: no service-port <spX> (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;