more cleanup

This commit is contained in:
Micky 2024-12-26 11:59:19 +11:00
parent 99fba8bd1e
commit a7d23846e6
4 changed files with 77 additions and 164 deletions

View File

@ -1,6 +1,4 @@
import yaml import os, yaml, logging
import os
import logging
from functools import wraps from functools import wraps
from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException
@ -54,21 +52,20 @@ def execute_command(device, command_format, target, ip_version):
device_config = { device_config = {
'device_type': device['type'], 'device_type': device['type'],
'host': device['host'], 'host': device['host'],
'port': device['port'],
'username': device['username'], 'username': device['username'],
'password': device['password'], 'password': device['password'],
'port': device.get('port', 22), 'timeout': 10,
'timeout': device.get('timeout', 10), 'session_timeout': 60,
'session_timeout': device.get('session_timeout', 60), 'conn_timeout': 10,
'conn_timeout': device.get('conn_timeout', 10), 'auth_timeout': 10,
'auth_timeout': device.get('auth_timeout', 10),
} }
command_timeout = device.get('command_timeout', 30) command_timeout = 30
try: try:
with establish_connection(device_config) as connection: with establish_connection(device_config) as connection:
# Format the command # Format the command
print(command_format)
command = str(command_format.format(ip_version=ip_version, target=target).strip()) command = str(command_format.format(ip_version=ip_version, target=target).strip())
# Execute the command # Execute the command
@ -82,45 +79,21 @@ def execute_command(device, command_format, target, ip_version):
# Clean output # Clean output
output = output.strip() if output else "" output = output.strip() if output else ""
logger.debug(f"Command output: {output}")
if not output: if not output:
return { logger.error(f"No response from {device['host']}")
'raw_output': "No output received from device", return {'error': True, 'message': 'No response from device'}
'structured_data': None,
'error': True,
'error_type': 'no_output'
}
return { return {'error': False, 'message': output}
'raw_output': output,
'error': False
}
except NetmikoTimeoutException as e: except NetmikoTimeoutException as e:
error_msg = f"Timeout error on {device['host']}: {e}" logger.error(f"Timeout error on {device['host']}: {e}")
logger.error(error_msg) return {'error': True, 'message': 'Timeout error'}
return {
'raw_output': error_msg,
'structured_data': None,
'error': True,
'error_type': 'timeout'
}
except NetmikoAuthenticationException as e: except NetmikoAuthenticationException as e:
error_msg = f"Authentication failed for {device['host']}: {e}" logger.error(f"Authentication failed for {device['host']}: {e}")
logger.error(error_msg) return {'error': True, 'message': 'Authentication failed'}
return {
'raw_output': error_msg,
'structured_data': None,
'error': True,
'error_type': 'auth'
}
except Exception as e: except Exception as e:
error_msg = f"An unexpected error occurred on {device['host']}: {e}" logger.error(f"An unexpected error occurred on {device['host']}: {e}")
logger.error(error_msg) return {'error': True, 'message': 'Unexpected error'}
return {
'raw_output': error_msg,
'structured_data': None,
'error': True,
'error_type': 'connection'
}

View File

@ -8,21 +8,20 @@ const app = Vue.createApp({
selectedIpVersion: 'IPv4', selectedIpVersion: 'IPv4',
isLoading: false, isLoading: false,
commandResult: '', commandResult: '',
devices: window.initialData?.devices || [], devices: window.initialData?.devices ?? [],
commands: window.initialData?.commands || [], commands: window.initialData?.commands ?? [],
currentCommand: null, currentCommand: null,
showHelp: false, showHelp: false,
showTerms: false, showTerms: false,
isOpen: false, isOpen: false,
isDark: localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches), isDark: this.getInitialTheme(),
site_config: window.initialData?.site_config || {},
peeringdbUrl: window.initialData?.site_config?.footer?.external_links?.peeringdb || '',
githubUrl: window.initialData?.site_config?.footer?.external_links?.github || ''
} }
}, },
mounted() { mounted() {
this.updateThemeClass(); this.updateThemeClass();
}, },
watch: { watch: {
selectedCommand: { selectedCommand: {
handler(newVal) { handler(newVal) {
@ -31,86 +30,88 @@ const app = Vue.createApp({
immediate: true immediate: true
} }
}, },
computed: { computed: {
showIpVersionSelector() { showIpVersionSelector() {
if (!this.targetIp) return true; if (!this.targetIp) return true;
// Check if the input is not a valid IP address const ipValidation = {
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; v4: /^(\d{1,3}\.){3}\d{1,3}$/,
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:){0,7}:([0-9a-fA-F]{1,4}:){0,7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^::ffff:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; v6: /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:){0,7}:([0-9a-fA-F]{1,4}:){0,7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^::ffff:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
};
// Detect IP version if (ipValidation.v6.test(this.targetIp)) {
if (ipv6Regex.test(this.targetIp)) {
this.selectedIpVersion = 'IPv6'; this.selectedIpVersion = 'IPv6';
return false; // Disable button for valid IPv6 return false;
} else if (ipv4Regex.test(this.targetIp)) { }
if (ipValidation.v4.test(this.targetIp)) {
this.selectedIpVersion = 'IPv4'; this.selectedIpVersion = 'IPv4';
return false; // Disable button for valid IPv4 return false;
} }
return true; // Enable button for non-IP input return true;
}, },
isValidInput() { isValidInput() {
if (!this.currentCommand || !this.targetIp) return false; if (!this.currentCommand || !this.targetIp) return false;
// If IP version selector is shown, any input is valid as it will be resolved
if (this.showIpVersionSelector) return true; if (this.showIpVersionSelector) return true;
if (this.currentCommand.field?.validation) { const { validation } = this.currentCommand.field ?? {};
const pattern = new RegExp(this.currentCommand.field.validation); return validation ? new RegExp(validation).test(this.targetIp) : true;
return pattern.test(this.targetIp);
}
return true;
} }
}, },
methods: { methods: {
getInitialTheme() {
return localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
},
toggleIpVersion() { toggleIpVersion() {
this.selectedIpVersion = this.selectedIpVersion === 'IPv4' ? 'IPv6' : 'IPv4'; this.selectedIpVersion = this.selectedIpVersion === 'IPv4' ? 'IPv6' : 'IPv4';
}, },
toggleTheme() { toggleTheme() {
this.isDark = !this.isDark; this.isDark = !this.isDark;
localStorage.theme = this.isDark ? 'dark' : 'light'; localStorage.theme = this.isDark ? 'dark' : 'light';
this.updateThemeClass(); this.updateThemeClass();
}, },
updateThemeClass() { updateThemeClass() {
if (this.isDark) { document.documentElement.classList[this.isDark ? 'add' : 'remove']('dark');
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, },
toggleDevice(device) { toggleDevice(device) {
// Update the selected device state
const wasDeselected = this.selectedDevice === device; const wasDeselected = this.selectedDevice === device;
this.selectedDevice = wasDeselected ? '' : device; this.selectedDevice = wasDeselected ? '' : device;
// Only reset states if we're deselecting the device entirely
if (wasDeselected) { if (wasDeselected) {
this.selectedCommand = ''; this.resetCommandState();
this.targetIp = '';
this.commandResult = '';
} }
// Force a re-render to ensure styling is updated this.$nextTick(() => this.$forceUpdate());
this.$nextTick(() => {
this.$forceUpdate();
});
}, },
resetCommandState() {
this.selectedCommand = '';
this.targetIp = '';
this.commandResult = '';
},
async executeCommand() { async executeCommand() {
if (!this.isValidInput) { if (!this.isValidInput) {
this.commandResult = '❌ Error: Please enter a valid input according to the command requirements'; this.commandResult = '❌ Error: Please enter a valid input.';
return; return;
} }
this.isLoading = true; this.isLoading = true;
this.commandResult = ''; this.commandResult = '';
try { try {
const response = await fetch('/execute', { const response = await fetch('/execute', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json',
},
body: JSON.stringify({ body: JSON.stringify({
device: this.selectedDevice, device: this.selectedDevice,
command: this.selectedCommand, command: this.selectedCommand,
@ -118,53 +119,35 @@ const app = Vue.createApp({
ipVersion: this.selectedIpVersion ipVersion: this.selectedIpVersion
}) })
}); });
const data = await response.json(); const data = await response.json();
if (!response.ok) { if (!response.ok || data.error) {
this.commandResult = `Error: ${data.message || 'An unknown error occurred'}`; this.commandResult = `Error: ${data.message || 'An error occurred.'}`;
return; return;
} }
if (data.error) { this.commandResult = data.message || 'Error: No output received from command.';
// Handle error responses with specific error types
const errorPrefix = data.error_type === 'timeout' ? '🕒 Timeout Error: ' :
data.error_type === 'auth' ? '🔒 Authentication Error: ' :
data.error_type === 'connection' ? '🔌 Connection Error: ' :
data.error_type === 'no_output' ? '📭 No Output Error: ' :
'❌ Error: ';
this.commandResult = errorPrefix + data.message;
return;
}
// Handle successful response
if (data.result) {
this.commandResult = data.result;
} else {
this.commandResult = '❌ Error: No output received from command';
}
} catch (error) { } catch (error) {
console.error('Command execution error:', error); this.commandResult = 'Error: An error occurred.';
this.commandResult = '❌ Error: Failed to execute command. Please try again.';
} finally { } finally {
this.isLoading = false; this.isLoading = false;
} }
}, },
toggleDropdown(event) { toggleDropdown(event) {
this.isOpen = !this.isOpen; this.isOpen = !this.isOpen;
}, },
closeDropdown(event) { closeDropdown(event) {
// Only close if clicking outside the dropdown
if (!event.target.closest('.relative')) { if (!event.target.closest('.relative')) {
this.isOpen = false; this.isOpen = false;
} }
}, },
selectCommand(command) { selectCommand(command) {
this.selectedCommand = command; this.selectedCommand = command;
// Add a small delay before closing to ensure the selection is visible setTimeout(() => this.isOpen = false, 100);
setTimeout(() => {
this.isOpen = false;
}, 100);
} }
} }
}); });

View File

@ -16,13 +16,6 @@
commands: {{ commands|tojson|safe }}, commands: {{ commands|tojson|safe }},
devices: {{ devices|tojson|safe }} devices: {{ devices|tojson|safe }}
}; };
// Check for saved theme preference or default to 'light'
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
</script> </script>
</head> </head>

View File

@ -1,5 +1,5 @@
from flask import ( from flask import (
Blueprint, request, jsonify, render_template Blueprint, request, render_template
) )
import logging import logging
@ -56,43 +56,7 @@ def execute():
ip_version = 6 if ip_version == "IPv6" else 4 ip_version = 6 if ip_version == "IPv6" else 4
try: # Execute the command using network_utils
# Execute the command using network_utils result = execute_command(device, command['format'], target, ip_version)
result = execute_command(device, command['format'], target, ip_version)
if not result: return result
error_msg = 'No response from command execution'
logger.error(error_msg)
return jsonify({
'error': True,
'message': error_msg,
'error_type': 'no_response'
})
# Check for error state
if result.get('error', False):
error_msg = result['raw_output']
logger.error(f"Command execution failed: {error_msg}")
return jsonify({
'error': True,
'message': error_msg,
'error_type': result.get('error_type', 'general')
})
# Log successful execution
logger.info(f"Successfully executed command {command} on {device}")
# Return successful result
return jsonify({
'error': False,
'result': result['raw_output'],
'structured_data': result.get('structured_data')
})
except Exception as e:
logger.error(f"Unexpected error during command execution: {str(e)}")
return jsonify({
'error': True,
'message': f"An unexpected error occurred: {str(e)}",
'error_type': 'general'
})