UNPKG

n8n-nodes-customssh

Version:

n8n community node for advanced SSH connections with configurable ciphers and network device support

986 lines 57.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CustomSsh = void 0; // CustomSsh.node.ts const n8n_workflow_1 = require("n8n-workflow"); const CipherUtils_1 = require("./utils/CipherUtils"); const SshCommandExecutor_1 = require("./services/SshCommandExecutor"); const ConfigManager_1 = require("./services/ConfigManager"); /** * Custom SSH Node for n8n that handles network device connections with varied SSH requirements */ class CustomSsh { constructor() { this.description = { displayName: 'Custom SSH', name: 'customSsh', icon: 'fa:terminal', group: ['input'], version: 2, subtitle: '={{$parameter["operation"]}}', description: 'Execute commands on network devices via SSH with configurable ciphers', defaults: { name: 'Custom SSH', color: '#000000', }, usableAsTool: true, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'sshPassword', required: true, }, ], properties: [ // Connection Properties { displayName: 'Operation', name: 'operation', type: 'options', options: [ { name: 'Execute Command', value: 'executeCommand', description: 'Execute a command on a remote device', action: 'Execute a command on a remote device', }, { name: 'Execute Multiple Commands', value: 'executeMultipleCommands', description: 'Execute multiple commands on a remote device', action: 'Execute multiple commands on a remote device', }, { name: 'Execute Command List', value: 'executeCommandList', description: 'Execute multiple commands from a text list (AI-friendly)', action: 'Execute commands from list', }, { name: 'Backup Configuration', value: 'backupConfig', description: 'Export device configuration', action: 'Backup device configuration', }, { name: 'Restore Configuration', value: 'restoreConfig', description: 'Restore configuration to device', action: 'Restore device configuration', }, ], default: 'executeCommand', noDataExpression: true, }, { displayName: 'Host', name: 'host', type: 'string', default: '', placeholder: 'e.g. 172.30.0.6', description: 'IPv4/IPv6 address or hostname of the device', required: true, }, { displayName: 'Port', name: 'port', type: 'number', default: 22, description: 'SSH port of the device', }, // SSH Configuration { displayName: 'Cipher Selection', name: 'cipherSelection', type: 'options', options: [ { name: 'All Available Ciphers (Including Legacy)', value: 'all' }, { name: 'Secure Ciphers Only', value: 'secure-only' }, { name: 'Legacy Ciphers Only', value: 'legacy-only' }, { name: 'AES 128-bit CTR', value: 'aes128-ctr' }, { name: 'AES 192-bit CTR', value: 'aes192-ctr' }, { name: 'AES 256-bit CTR', value: 'aes256-ctr' }, { name: 'AES 128-bit GCM', value: 'aes128-gcm@openssh.com' }, { name: 'AES 256-bit GCM', value: 'aes256-gcm@openssh.com' }, { name: 'ChaCha20-Poly1305', value: 'chacha20-poly1305@openssh.com' }, // Legacy ciphers { name: 'AES 128-bit CBC (Legacy)', value: 'aes128-cbc' }, { name: 'AES 192-bit CBC (Legacy)', value: 'aes192-cbc' }, { name: 'AES 256-bit CBC (Legacy)', value: 'aes256-cbc' }, { name: '3DES CBC (Legacy)', value: '3des-cbc' }, ], default: 'all', description: 'Select specific cipher or cipher group for the SSH connection', }, // Command Execution { displayName: 'Command', name: 'command', type: 'string', displayOptions: { show: { operation: ['executeCommand'], }, }, default: '', placeholder: 'e.g. show version', description: 'Command to execute on the remote device', required: true, }, { displayName: 'Commands', name: 'commands', type: 'fixedCollection', placeholder: 'Add Command', displayOptions: { show: { operation: ['executeMultipleCommands'], }, }, typeOptions: { multipleValues: true, }, description: 'Commands to execute', default: {}, options: [ { name: 'commandsArray', displayName: 'Commands', values: [ { displayName: 'Command', name: 'command', type: 'string', default: '', description: 'The command to execute', required: true, }, { displayName: 'Wait Time (ms)', name: 'waitTime', type: 'number', default: 1000, description: 'Time to wait after executing this command before sending the next one', }, ], }, ], }, // Command List Parameters (AI-friendly) { displayName: 'Commands Format', name: 'commandsFormat', type: 'options', displayOptions: { show: { operation: ['executeCommandList'], }, }, options: [ { name: 'Text List (One Command Per Line)', value: 'textList', description: 'Commands separated by new lines', }, { name: 'JSON Array', value: 'jsonArray', description: 'Array of command objects in JSON format', }, { name: 'Simple Array', value: 'simpleArray', description: 'Array of command strings', }, ], default: 'textList', }, { displayName: 'Commands', name: 'commandsList', type: 'string', typeOptions: { rows: 10, }, displayOptions: { show: { operation: ['executeCommandList'], commandsFormat: ['textList'], }, }, default: '', placeholder: 'show version\nshow interfaces\nshow ip route', description: 'Enter commands separated by new lines', required: true, }, { displayName: 'Commands JSON', name: 'commandsJson', type: 'json', displayOptions: { show: { operation: ['executeCommandList'], commandsFormat: ['jsonArray', 'simpleArray'], }, }, default: '["show version", "show interfaces"]', description: 'JSON array of commands or command objects', required: true, }, { displayName: 'Default Wait Time (ms)', name: 'defaultWaitTime', type: 'number', displayOptions: { show: { operation: ['executeCommandList'], }, }, default: 1000, description: 'Default time to wait between commands (milliseconds)', }, // Config Backup Parameters { displayName: 'Configuration Type', name: 'configType', type: 'options', displayOptions: { show: { operation: ['backupConfig'], }, }, options: [ { name: 'Running Configuration', value: 'running', description: 'Current active configuration', }, { name: 'Startup Configuration', value: 'startup', description: 'Configuration used at boot', }, ], default: 'running', description: 'Type of configuration to retrieve', }, { displayName: 'Return Type', name: 'returnType', type: 'options', displayOptions: { show: { operation: ['backupConfig'], }, }, options: [ { name: 'Return Config as Text', value: 'text', description: 'Return configuration as text in the output', }, { name: 'Binary File', value: 'binary', description: 'Return configuration as a binary file', }, ], default: 'text', }, // Config Restore Parameters { displayName: 'Configuration Source', name: 'configSource', type: 'options', displayOptions: { show: { operation: ['restoreConfig'], }, }, options: [ { name: 'Text Input', value: 'text', description: 'Directly enter configuration commands', }, { name: 'Binary Data', value: 'binary', description: 'Use binary data (from previous node)', }, ], default: 'text', }, { displayName: 'Configuration Commands', name: 'configCommands', type: 'string', typeOptions: { rows: 10, }, displayOptions: { show: { operation: ['restoreConfig'], configSource: ['text'], }, }, default: '', placeholder: 'hostname router1\ninterface GigabitEthernet0/1\n description WAN\n ip address dhcp', description: 'Configuration commands to apply', required: true, }, { displayName: 'Binary Property', name: 'binaryProperty', type: 'string', displayOptions: { show: { operation: ['restoreConfig'], configSource: ['binary'], }, }, default: 'data', description: 'Name of the binary property containing the configuration file', required: true, }, { displayName: 'Apply Method', name: 'applyMethod', type: 'options', displayOptions: { show: { operation: ['restoreConfig'], }, }, options: [ { name: 'Merge (Add to existing)', value: 'merge', description: 'Add/update to current configuration', }, { name: 'Replace (Dangerous)', value: 'replace', description: 'Replace entire configuration', }, ], default: 'merge', description: 'How to apply the configuration', }, { displayName: 'Save After Apply', name: 'saveAfterApply', type: 'boolean', displayOptions: { show: { operation: ['restoreConfig'], }, }, default: true, description: 'Save configuration after applying', }, { displayName: 'Advanced Security Options', name: 'advancedSecurity', type: 'collection', placeholder: 'Add Option', default: {}, options: [ { displayName: 'Compatibility Level', name: 'compatibilityLevel', type: 'options', options: [ { name: 'High (Most Compatible, Less Secure)', value: 'high' }, { name: 'Medium (Balance of Compatibility and Security)', value: 'medium' }, { name: 'Legacy Only (For Very Old Devices)', value: 'legacy-only' }, { name: 'Modern Only (Most Secure, Least Compatible)', value: 'modern-only' }, ], default: 'medium', description: 'Compatibility level for key exchange and encryption algorithms', }, { displayName: 'Security Level', name: 'securityLevel', type: 'options', options: [ { name: 'High (Most Secure)', value: 'high' }, { name: 'Medium (Balance)', value: 'medium' }, { name: 'Low (Most Compatible)', value: 'low' }, ], default: 'medium', description: 'Security level for MAC and host key algorithms', }, { displayName: 'Allow Legacy Algorithms', name: 'allowLegacyAlgorithms', type: 'boolean', default: true, description: 'Allow older, less secure algorithms for maximum compatibility', }, ], }, // Connection Settings { displayName: 'Connection Options', name: 'connectionOptions', type: 'collection', placeholder: 'Add Option', default: {}, options: [ // Basic connection options { displayName: 'Connection Timeout', name: 'connTimeout', type: 'number', default: 10000, description: 'Connection timeout in milliseconds', }, { displayName: 'Login Prompt Timeout', name: 'promptTimeout', type: 'number', default: 8000, description: 'Timeout for login prompt detection in milliseconds', }, { displayName: 'Username Prompt', name: 'usernamePrompt', type: 'string', default: 'login:|username:|user:', description: 'Regular expression to match username prompt', }, { displayName: 'Password Prompt', name: 'passwordPrompt', type: 'string', default: 'password:|Password:', description: 'Regular expression to match password prompt', }, { displayName: 'Command Prompt', name: 'commandPrompt', type: 'string', default: '[#>$]\\s*$', description: 'Regular expression to match command prompt', }, { displayName: 'Try Fallback Ciphers', name: 'fallbackCiphers', type: 'boolean', default: true, description: 'Automatically try other ciphers if the selected one fails', }, { displayName: 'Retry Count', name: 'retryCount', type: 'number', default: 3, description: 'Number of retries on connection failure', }, { displayName: 'Retry Delay', name: 'retryDelay', type: 'number', default: 3000, description: 'Delay between retries in milliseconds', }, { displayName: 'Verbose Logging', name: 'verboseLogging', type: 'boolean', default: false, description: 'Enable detailed logging (not recommended for production)', }, // Advanced options { displayName: 'Send Initial CR', name: 'sendInitialCR', type: 'boolean', default: true, description: 'Send carriage return on connect to stimulate prompt', }, { displayName: 'Terminal Type', name: 'terminalType', type: 'options', options: [ { name: 'VT100', value: 'vt100' }, { name: 'VT102', value: 'vt102' }, { name: 'VT220', value: 'vt220' }, { name: 'Ansi', value: 'ansi' }, { name: 'Xterm', value: 'xterm' }, ], default: 'vt100', description: 'Terminal type to use for the SSH connection', }, { displayName: 'Allow Empty Prompt', name: 'allowEmptyPrompt', type: 'boolean', default: false, description: "Allow empty prompt response (use for devices that don't send initial prompt)", }, { displayName: 'Debug Buffer Content', name: 'debugBuffer', type: 'boolean', default: false, description: 'Enable detailed buffer content debugging (shows received bytes in hex)', }, { displayName: 'Initial Command', name: 'initialCommand', type: 'string', default: '', description: 'Command to send immediately after connection (helpful for non-standard SSH devices)', }, { displayName: 'Enable Privilege Mode', name: 'enablePrivilegeMode', type: 'boolean', default: false, description: 'Send enable command after connecting (for Cisco and similar devices)', }, { displayName: 'Privilege Mode Password', name: 'privilegeModePassword', type: 'string', typeOptions: { password: true, }, default: '', displayOptions: { show: { enablePrivilegeMode: [true], }, }, description: 'Password for the enable command (if different from login password)', }, { displayName: 'Line Ending Style', name: 'lineEnding', type: 'options', options: [ { name: 'CR+LF (\\r\\n)', value: '\r\n' }, { name: 'LF only (\\n)', value: '\n' }, { name: 'CR only (\\r)', value: '\r' }, ], default: '\r\n', description: 'Line ending characters to use when sending commands', }, // Device-specific options { displayName: 'Device Type', name: 'deviceType', type: 'options', options: [ { name: 'Generic', value: 'generic' }, { name: 'Aruba CX Switch', value: 'aruba' }, { name: 'Aruba OS Switch', value: 'aruba-os' }, { name: 'Aruba Access Point', value: 'aruba-ap' }, { name: 'Aruba Gateway', value: 'aruba-gw' }, { name: 'Cisco IOS', value: 'cisco' }, { name: 'Juniper', value: 'juniper' }, { name: 'Legacy Switch (CBC Ciphers)', value: 'legacy-switch' }, ], default: 'generic', description: 'Select device type for vendor-specific optimizations', }, ], }, ], }; } async execute() { var _a, _b; const items = this.getInputData(); const returnData = []; // Process each item from the input for (let i = 0; i < items.length; i++) { try { // Get node parameters const operation = this.getNodeParameter('operation', i); const host = this.getNodeParameter('host', i); const port = this.getNodeParameter('port', i); const cipherSelection = this.getNodeParameter('cipherSelection', i); const connectionOptions = this.getNodeParameter('connectionOptions', i, {}); // Get credentials const credentials = await this.getCredentials('sshPassword'); if (!credentials) { throw new Error('No credentials provided for SSH connection'); } const username = credentials.username; const password = credentials.password; // Configure ciphers based on selection const ciphers = CipherUtils_1.CipherUtils.configureCiphers(cipherSelection); const advancedSecurity = this.getNodeParameter('advancedSecurity', i, {}); // Set connection options const options = { connTimeout: connectionOptions.connTimeout || 10000, promptTimeout: connectionOptions.promptTimeout || 8000, usernamePromptRegex: new RegExp(connectionOptions.usernamePrompt || 'login:|username:|user:', 'i'), passwordPromptRegex: new RegExp(connectionOptions.passwordPrompt || 'password:|Password:', 'i'), commandPromptRegex: new RegExp(connectionOptions.commandPrompt || '\\S+[#>$]\\s*$|#\\s*$'), fallbackCiphers: connectionOptions.fallbackCiphers !== false, retryCount: connectionOptions.retryCount || 3, retryDelay: connectionOptions.retryDelay || 3000, verboseLogging: connectionOptions.verboseLogging || false, // Add default values for new properties sendInitialCR: (_a = connectionOptions.sendInitialCR) !== null && _a !== void 0 ? _a : true, terminalType: connectionOptions.terminalType || 'vt100', allowEmptyPrompt: connectionOptions.allowEmptyPrompt || false, debugBuffer: connectionOptions.debugBuffer || false, initialCommand: connectionOptions.initialCommand || '', enablePrivilegeMode: connectionOptions.enablePrivilegeMode || false, privilegeModePassword: connectionOptions.privilegeModePassword || '', lineEnding: connectionOptions.lineEnding || '\r\n', deviceType: connectionOptions.deviceType || 'generic', deviceSpecific: {}, // Add stable output detection stableOutputDetection: true, lastDataTime: Date.now(), // Add advanced security options advancedSecurity: { compatibilityLevel: advancedSecurity.compatibilityLevel || 'medium', securityLevel: advancedSecurity.securityLevel || 'medium', allowLegacyAlgorithms: advancedSecurity.allowLegacyAlgorithms !== false, }, }; // For older switches, update device-specific settings if (connectionOptions.deviceType === 'legacy-switch') { options.commandPromptRegex = new RegExp('\\S+[#>]\\s*$'); options.terminalType = 'vt100'; options.lineEnding = '\r\n'; options.advancedSecurity = { compatibilityLevel: 'high', securityLevel: 'low', allowLegacyAlgorithms: true, }; } // Apply device-specific settings if (connectionOptions.deviceType === 'aruba') { // Use a regex that handles the Aruba CX prompt pattern with or without trailing \r\n options.commandPromptRegex = new RegExp('\\S+#\\s*(?:\\r\\n)?$'); options.terminalType = 'vt100'; options.lineEnding = '\r\n'; options.sendInitialCR = true; options.promptTimeout = Math.max(options.promptTimeout, 15000); // At least 15 seconds options.stableOutputDetection = true; options.deviceSpecific = { arubaMode: true, // Add specific known prompts for exact matching knownPrompts: ['ox60-mdf-c2-sw1#', 'homelab-6300#'], }; } else if (connectionOptions.deviceType === 'aruba-os') { // Aruba OS Switch specific configuration options.commandPromptRegex = new RegExp('(?:\\\\S+|)[#>]\\\\s*$'); options.terminalType = 'vt100'; options.lineEnding = '\r\n'; options.sendInitialCR = true; options.promptTimeout = Math.max(options.promptTimeout, 20000); // Longer timeout for Aruba OS options.stableOutputDetection = true; options.debugBuffer = true; // Enable buffer debugging for Aruba OS by default options.advancedSecurity = { compatibilityLevel: 'high', // More compatible securityLevel: 'low', // For better compatibility allowLegacyAlgorithms: true, // Allow legacy algorithms }; options.deviceSpecific = { arubaOsMode: true, // Add your specific hostname prompt pattern knownPrompts: ['ofer-3-sw1#', '>', '#', '(config)#', '(config-if)#', '(config-vlan)#'], // Specific handling flags allowMultiplePromptPatterns: true, handlePagination: true, paginationPrompt: '--MORE--', paginationContinue: ' ', }; // Set the initial command to 'no pag' to disable pagination if possible options.initialCommand = 'no pag'; } else if (connectionOptions.deviceType === 'aruba-ap') { // Aruba Access Point specific configuration options.commandPromptRegex = new RegExp('\\S+#\\s*$|\\S+>\\s*$'); options.terminalType = 'vt100'; options.lineEnding = '\r\n'; options.sendInitialCR = true; options.promptTimeout = Math.max(options.promptTimeout, 15000); // APs may be slower options.stableOutputDetection = true; options.debugBuffer = true; // Enable debugging for AP connections options.advancedSecurity = { compatibilityLevel: 'high', // Maximum compatibility for APs securityLevel: 'low', // Lower security for better AP compatibility allowLegacyAlgorithms: true, // APs often need legacy algorithms }; options.deviceSpecific = { arubaApMode: true, // Common AP prompt patterns knownPrompts: ['#', '>', 'ap#', 'instant#', 'User:', 'Password:', 'login:'], // AP-specific handling allowMultiplePromptPatterns: true, handlePagination: true, paginationPrompt: '--More--', paginationContinue: ' ', // AP connection characteristics slowConnectionMode: true, requiresLegacyCiphers: true, // Authentication characteristics authRetryDelay: 2000, supportKeyboardInteractive: true, }; // Update authentication prompt patterns for APs options.usernamePromptRegex = new RegExp('login:|username:|user:|User:', 'i'); options.passwordPromptRegex = new RegExp('password:|Password:', 'i'); // Aruba APs don't support pagination disable commands - no initial command needed } else if (connectionOptions.deviceType === 'aruba-gw') { // Aruba Gateway/Mobility Controller specific configuration options.commandPromptRegex = new RegExp('\\([^)]*\\)\\s*[#>]\\s*$|\\S+[#>]\\s*$'); options.terminalType = 'vt100'; options.lineEnding = '\r\n'; options.sendInitialCR = true; options.promptTimeout = Math.max(options.promptTimeout, 18000); // Gateway may need more time options.stableOutputDetection = true; options.debugBuffer = true; // Enable debugging for gateway connections too options.advancedSecurity = { compatibilityLevel: 'high', // Higher compatibility for gateways securityLevel: 'low', // Lower security for better compatibility allowLegacyAlgorithms: true, // Some legacy algorithm support }; options.deviceSpecific = { arubaGwMode: true, // Common gateway prompt patterns knownPrompts: [ '(host) #', '(config) #', '#', '>', '(host) >', '(config) >', 'User:', 'Password:', 'login:', ], // Gateway-specific handling allowMultiplePromptPatterns: true, handlePagination: true, paginationPrompt: '--More--', paginationContinue: ' ', // Gateway connection characteristics supportFullCli: true, configModeSupport: true, // Authentication characteristics authRetryDelay: 1500, supportKeyboardInteractive: true, }; // Update authentication prompt patterns for Gateways options.usernamePromptRegex = new RegExp('login:|username:|user:|User:', 'i'); options.passwordPromptRegex = new RegExp('password:|Password:', 'i'); // Disable pagination for gateways options.initialCommand = 'no pag'; } else if (connectionOptions.deviceType === 'cisco') { options.commandPromptRegex = new RegExp('\\S+[#>]\\s*$'); options.promptTimeout = Math.max(options.promptTimeout, 10000); options.deviceSpecific = { ciscoMode: true }; } else if (connectionOptions.deviceType === 'juniper') { options.commandPromptRegex = new RegExp('\\S+@\\S+[>%]\\s*$'); options.deviceSpecific = { juniperMode: true }; } // Execute commands based on the operation if (operation === 'executeCommand') { const command = this.getNodeParameter('command', i); const result = await SshCommandExecutor_1.SshCommandExecutor.executeSshCommand(host, port, username, password, command, ciphers, options); returnData.push({ json: { success: true, host, ...result, }, }); } else if (operation === 'executeMultipleCommands') { const commandsCollection = this.getNodeParameter('commands.commandsArray', i, []); // Extract commands and wait times const commands = commandsCollection.map((item) => ({ command: item.command, waitTime: item.waitTime || 1000, })); const result = await SshCommandExecutor_1.SshCommandExecutor.executeMultipleSshCommands(host, port, username, password, commands, ciphers, options); returnData.push({ json: { success: true, host, ...result, }, }); } else if (operation === 'executeCommandList') { const commandsFormat = this.getNodeParameter('commandsFormat', i); const defaultWaitTime = this.getNodeParameter('defaultWaitTime', i, 1000); let commandList = []; // Parse commands based on format - improved parsing logic if (commandsFormat === 'textList') { const textCommands = this.getNodeParameter('commandsList', i); // Split by newlines and also by semicolons for better compatibility const rawCommands = textCommands .split('\n') .flatMap((line) => line.split(';')) .map((cmd) => cmd.trim()) .filter((cmd) => cmd.length > 0); commandList = rawCommands.map((cmd) => ({ command: cmd, waitTime: defaultWaitTime, })); // Log parsed commands for debugging if (options.verboseLogging) { console.log(`[SSH Node] Parsed ${commandList.length} commands from text input`); commandList.forEach((cmd, index) => { console.log(`[SSH Node] Command ${index + 1}: "${cmd.command}"`); }); } } else { // Handle JSON formats with better error checking const jsonInput = this.getNodeParameter('commandsJson', i); let parsedJson; try { parsedJson = JSON.parse(jsonInput); } catch (error) { throw new Error(`Invalid JSON format: ${error.message}`); } if (commandsFormat === 'simpleArray') { // Input is a simple array of strings if (!Array.isArray(parsedJson)) { throw new Error('Expected an array of command strings'); } commandList = parsedJson.map((cmd) => { const cmdStr = String(cmd).trim(); return { command: cmdStr, waitTime: defaultWaitTime, }; }); } else { // Input is array of objects if (!Array.isArray(parsedJson)) { throw new Error('Expected an array of command objects'); } commandList = parsedJson.map((item) => { if (typeof item === 'string') { return { command: item.trim(), waitTime: defaultWaitTime }; } else if (typeof item === 'object' && item !== null) { if (!item.command) { throw new Error('Each command object must contain a "command" property'); } return { command: String(item.command).trim(), waitTime: item.waitTime !== undefined ? parseInt(String(item.waitTime), 10) : defaultWaitTime, }; } else { throw new Error('Invalid command format in JSON array'); } }); } // Log parsed commands for debugging if (options.verboseLogging) { console.log(`[SSH Node] Parsed ${commandList.length} commands from JSON input`); commandList.forEach((cmd, index) => { console.log(`[SSH Node] Command ${index + 1}: "${cmd.command}" (wait: ${cmd.waitTime}ms)`); }); } } // Validate commands if (commandList.length === 0) { throw new Error('No valid commands provided'); } // Adjust wait time based on device type if (options.deviceType === 'aruba') { commandList = commandList.map((cmd) => ({ ...cmd, waitTime: Math.max(cmd.waitTime, 1500), // At least 1.5 second wait for Aruba CX })); } else if (options.deviceType === 'aruba-os') { commandList = commandList.map((cmd) => ({ ...cmd, waitTime: Math.max(cmd.waitTime, 2000), // At least 2 second wait for Aruba OS })); // For Aruba OS, add pagination handling command if not already included if (!commandList.some((cmd) => cmd.command.includes('no pag'))) { commandList.unshift({ command: 'no pag', waitTime: 1000, }); } // For Aruba OS, add pagination handling command if not already included if (!commandList.some((cmd) => cmd.command.includes('no pag'))) { commandList.unshift({ command: 'no pag', waitTime: 1000, }); } } else if (options.deviceType === 'aruba-ap') { commandList = commandList.map((cmd) => ({ ...cmd, waitTime: Math.max(cmd.waitTime, 1000), // At least 1 second wait for Aruba APs })); // Aruba APs don't support pagination disable commands like 'no pag' // Remove any pagination commands that might have been added commandList = commandList.filter((cmd) => !cmd.command.includes('no pag')); } else if (options.deviceType === 'aruba-gw') { commandList = commandList.map((cmd) => ({ ...cmd, waitTime: Math.max(cmd.waitTime, 2000), // At least 2 second wait for Aruba Gateways })); // For Aruba Gateways, add pagination handling command if not already included if (!commandList.some((cmd) => cmd.command.includes('no pag'))) { commandList.unshift({ command: 'no pag', waitTime: 1000, }); } } // Execute commands with enhanced timeout const execOptions = { ...options, promptTimeout: Math.max(options.promptTimeout, 15000), // Ensure sufficient timeout }; if (options.verboseLogging) { console.log(`[SSH Node] Executing ${commandList.length} commands on ${host}`); } const result = await SshCommandExecutor_1.SshCommandExecutor.executeMultipleSshCommands(host, port, username, password, commandList, ciphers, execOptions); if (options.verboseLogging && result.results) { console.log(`[SSH Node] Command execution complete. Got ${result.results.length} results`); result.results.forEach((cmdResult, index) => { console.log(`[SSH Node] Result ${index + 1}: Command "${cmdResult.command}" produced ${cmdResult.output.length} bytes of output`); }); } returnData.push({ json: { success: true, host, commandCount: commandList.length, results: result.results, cipher: result.cipher, }, }); } else if (operation === 'backupConfig') { const configType = this.getNodeParameter('configType', i); const returnType = this.getNodeParameter('returnType', i); // Adjust commands for Aruba OS switches if needed if (options.deviceType === 'aruba-os') { // Aruba OS uses different commands for c