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
JavaScript
"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