UNPKG

n8n-nodes-netdevices

Version:

n8n node to interact with network devices (Cisco, Juniper, Palo Alto PAN-OS, Ciena SAOS, Linux, etc.)

377 lines 15.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LinuxConnection = void 0; const base_connection_1 = require("../base-connection"); let Logger; try { Logger = require('n8n-workflow').LoggerProxy; } catch (error) { Logger = { debug: (...args) => console.log('[DEBUG]', ...args), info: (...args) => console.log('[INFO]', ...args), warn: (...args) => console.warn('[WARN]', ...args), error: (...args) => console.error('[ERROR]', ...args) }; } class LinuxConnection extends base_connection_1.BaseConnection { constructor(credentials) { super(credentials); this.basePrompt = ''; this.rootUser = false; } async sessionPreparation() { Logger.debug('Starting Linux session preparation', { host: this.credentials.host, fastMode: this.fastMode }); try { await this.createLinuxShellChannel(); await this.setBasePrompt(); Logger.debug('Linux session preparation completed successfully'); } catch (error) { Logger.error('Linux session preparation failed', { error: error instanceof Error ? error.message : String(error) }); throw error; } } async createLinuxShellChannel() { return new Promise((resolve, reject) => { Logger.debug('Creating Linux shell channel'); if (!this.client) { reject(new Error('SSH client not available')); return; } this.client.shell((err, channel) => { if (err) { Logger.error('Failed to create shell channel', { error: err.message }); reject(err); return; } this.currentChannel = channel; setTimeout(() => { Logger.debug('Shell channel created successfully'); resolve(); }, 2000); }); }); } stripAnsi(str) { return str.replace(/[\u001b\u009b][[()#;?]*.{0,2}(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); } async setBasePrompt() { Logger.debug('Setting base prompt using Netmiko-style pattern matching.'); await this.writeChannel(this.newline); const linuxPromptRegex = '[#$>]\\s*$'; let output = ''; try { output = await this.readUntilPattern(linuxPromptRegex, 5000, true); } catch (e) { Logger.warn('Could not find prompt with initial method. Trying a second time.'); await this.readChannel(100).catch(() => { }); await this.writeChannel(this.newline); output = await this.readUntilPattern(linuxPromptRegex, 5000, true); } const cleanOutput = this.stripAnsi(output); const lines = cleanOutput.trim().split('\n'); const newPrompt = lines[lines.length - 1].trim(); if (newPrompt) { this.basePrompt = newPrompt; Logger.info('Determined base prompt', { basePrompt: this.basePrompt }); } else { Logger.error('All prompt detection methods failed. Using a generic regex as a last resort.'); this.basePrompt = linuxPromptRegex; } await this.disablePaging(); } async sendCommand(command) { return new Promise((resolve, reject) => { if (!this.isConnected || !this.client) { const err = new Error('Not connected to device'); Logger.error('sendCommand: ' + err.message); return reject(err); } Logger.debug('Executing Linux command via client.exec()', { command }); let output = ''; let errorOutput = ''; let streamClosed = false; let timeoutId; const cleanup = () => { if (timeoutId) clearTimeout(timeoutId); }; this.client.exec(command, (err, stream) => { if (err) { Logger.error('sendCommand: Failed to execute command', { command, error: err.message }); cleanup(); return reject(err); } timeoutId = setTimeout(() => { const msg = `sendCommand: Command timeout after ${this.commandTimeout}ms`; Logger.error(msg, { command }); cleanup(); stream.close(); reject(new Error(msg)); }, this.commandTimeout); stream.on('data', (data) => { const chunk = data.toString('utf8'); output += chunk; Logger.debug('sendCommand: stdout data received', { command, length: chunk.length }); }); stream.stderr.on('data', (data) => { const chunk = data.toString('utf8'); errorOutput += chunk; Logger.warn('sendCommand: stderr data received', { command, length: chunk.length }); }); stream.on('close', (code, signal) => { if (streamClosed) return; streamClosed = true; Logger.debug('sendCommand: stream closed', { command, code, signal }); cleanup(); if (errorOutput && !output) { resolve({ command, output: this.stripAnsi(errorOutput).trim(), success: false, error: `Command failed with exit code ${code || 'N/A'}` }); } else { const fullOutput = errorOutput ? `${output}\n--- STDERR ---\n${errorOutput}` : output; resolve({ command, output: this.stripAnsi(fullOutput).trim(), success: code === 0, error: code !== 0 ? `Command failed with exit code ${code || 'N/A'}` : undefined }); } }); stream.on('error', (streamErr) => { Logger.error('sendCommand: stream error', { command, error: streamErr.message }); cleanup(); reject(streamErr); }); }); }); } async readUntilPattern(pattern, timeout = 10000, isRegex = false) { let buffer = ''; const promptRegex = isRegex ? new RegExp(pattern) : new RegExp(this.escapeRegex(pattern)); return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { cleanup(); const msg = `Timeout waiting for pattern: ${pattern}. Last buffer content: ${buffer.slice(-1000)}`; Logger.error('Read until pattern timeout', { pattern, timeout, bufferLength: buffer.length, bufferSample: buffer.slice(-1000), }); reject(new Error(msg)); }, timeout); const onData = (data) => { const chunk = data.toString('utf8'); buffer += chunk; Logger.debug('readUntilPattern: data received', { length: chunk.length, totalBuffer: buffer.length }); setTimeout(() => { if (this.currentChannel && promptRegex.test(buffer)) { Logger.debug('readUntilPattern: prompt found', { pattern }); cleanup(); resolve(buffer); } }, 50); }; const onError = (err) => { Logger.error('readUntilPattern: onError event', { error: err.message, stack: err.stack, isChannel: !!this.currentChannel, isReadable: this.currentChannel ? this.currentChannel.readable : false, isWritable: this.currentChannel ? this.currentChannel.writable : false, isDestroyed: this.currentChannel ? this.currentChannel.destroyed : false, }); cleanup(); reject(err); }; const onClose = () => { Logger.warn('readUntilPattern: onClose event. Channel is closing.', { isChannel: !!this.currentChannel, isReadable: this.currentChannel ? this.currentChannel.readable : false, isWritable: this.currentChannel ? this.currentChannel.writable : false, isDestroyed: this.currentChannel ? this.currentChannel.destroyed : false, bufferLength: buffer.length, bufferSample: buffer.slice(-200), }); cleanup(); if (promptRegex.test(buffer)) { Logger.debug('readUntilPattern: prompt found in buffer after onClose event.'); resolve(buffer); } else { reject(new Error('Channel closed while waiting for pattern.')); } }; const cleanup = () => { clearTimeout(timeoutId); if (this.currentChannel) { this.currentChannel.removeListener('data', onData); this.currentChannel.removeListener('error', onError); this.currentChannel.removeListener('close', onClose); } }; if (this.currentChannel) { this.currentChannel.on('data', onData); this.currentChannel.on('error', onError); this.currentChannel.on('close', onClose); Logger.debug('readUntilPattern: Listeners attached.'); } else { cleanup(); reject(new Error('No active channel available for reading.')); } }); } async readChannel(timeout = 2000) { return new Promise((resolve, reject) => { let buffer = ''; let timeoutId; const onData = (data) => { buffer += data.toString('utf8'); }; const cleanup = () => { if (this.currentChannel) { this.currentChannel.removeListener('data', onData); } clearTimeout(timeoutId); }; timeoutId = setTimeout(() => { cleanup(); resolve(buffer); }, timeout); if (this.currentChannel) { this.currentChannel.on('data', onData); } else { reject(new Error('No active channel to read from.')); } }); } escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } sanitizeOutput(output, command) { Logger.debug('Sanitizing Linux output', { command, originalLength: output.length }); let lines = output.split(/\r?\n/); const commandIndex = lines.findIndex(line => line.includes(command)); if (commandIndex !== -1) { lines.splice(commandIndex, 1); } if (lines.length > 0) { const lastLine = lines[lines.length - 1]; const promptPattern = new RegExp(this.basePrompt); const promptFound = promptPattern.test(lastLine); if (promptFound) { lines.pop(); } } return lines.join('\n').trim(); } async disablePaging() { Logger.debug('Disabling terminal paging for Linux'); const commands = [ 'stty -echo', 'stty cols 512', 'export HISTSIZE=0', 'export HISTFILESIZE=0', ]; for (const cmd of commands) { await this.writeChannel(cmd + this.newline); await new Promise(resolve => setTimeout(resolve, 150)); } await this.readChannel(500); Logger.debug('Terminal paging disabled.'); } async sendConfig(configCommands) { try { let allOutput = ''; for (const command of configCommands) { const result = await this.sendCommand(command); if (!result.success) { throw new Error(`Command failed: ${command} - ${result.error}`); } allOutput += result.output + '\n'; } return { command: configCommands.join('; '), output: allOutput.trim(), success: true }; } catch (error) { return { command: configCommands.join('; '), output: '', success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } async getCurrentConfig() { return await this.sendCommand('cat /etc/os-release && echo "---" && uname -a'); } async saveConfig() { return await this.sendCommand('sync && echo "Configuration synchronized"'); } async rebootDevice() { const command = this.rootUser ? 'reboot' : 'sudo reboot'; return await this.sendCommand(command); } isRootUser() { return this.rootUser; } async executeAsRoot(command) { if (this.rootUser) { return await this.sendCommand(command); } else { return await this.sendCommand('sudo ' + command); } } async getSystemInfo() { return await this.sendCommand('uname -a && cat /etc/os-release'); } async getProcessList() { return await this.sendCommand('ps aux'); } async getDiskUsage() { return await this.sendCommand('df -h'); } async getMemoryInfo() { return await this.sendCommand('free -h'); } async getNetworkInterfaces() { return await this.sendCommand('ip addr show'); } async getServiceStatus(serviceName) { return await this.sendCommand(`systemctl status ${serviceName}`); } async startService(serviceName) { return await this.executeAsRoot(`systemctl start ${serviceName}`); } async stopService(serviceName) { return await this.executeAsRoot(`systemctl stop ${serviceName}`); } async restartService(serviceName) { return await this.executeAsRoot(`systemctl restart ${serviceName}`); } } exports.LinuxConnection = LinuxConnection; //# sourceMappingURL=linux-connection.js.map