more cleanup, add webhook support
This commit is contained in:
parent
a7d23846e6
commit
d2dd1525f5
@ -1,9 +1,10 @@
|
|||||||
import os, yaml, logging
|
import os, yaml, logging, requests
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException
|
from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Exception handler
|
# Exception handler
|
||||||
def exception_handler(func):
|
def exception_handler(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
@ -15,8 +16,10 @@ def exception_handler(func):
|
|||||||
return "An error occurred", 500
|
return "An error occurred", 500
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
# Load YAML configuration file
|
||||||
@exception_handler
|
@exception_handler
|
||||||
def load_yaml(filename):
|
def load_yaml(filename, key=None):
|
||||||
if not filename.endswith('.yaml'):
|
if not filename.endswith('.yaml'):
|
||||||
filename = f"{filename}.yaml"
|
filename = f"{filename}.yaml"
|
||||||
|
|
||||||
@ -32,7 +35,13 @@ def load_yaml(filename):
|
|||||||
if data is None:
|
if data is None:
|
||||||
logger.warning(f"Empty configuration file: {filename}")
|
logger.warning(f"Empty configuration file: {filename}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
# Return specific key if provided
|
||||||
|
if key:
|
||||||
|
return data.get(key, None)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except yaml.YAMLError as e:
|
except yaml.YAMLError as e:
|
||||||
logger.error(f"YAML parsing error in {filename}: {e}")
|
logger.error(f"YAML parsing error in {filename}: {e}")
|
||||||
return {}
|
return {}
|
||||||
@ -41,6 +50,21 @@ def load_yaml(filename):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
# Send data to a webhook URL
|
||||||
|
@exception_handler
|
||||||
|
def send_webhook(webhook_url, text_data):
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
"text": text_data
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(webhook_url, json=payload, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
|
# Establish connection to network device
|
||||||
def establish_connection(device_config):
|
def establish_connection(device_config):
|
||||||
try:
|
try:
|
||||||
return ConnectHandler(**device_config)
|
return ConnectHandler(**device_config)
|
||||||
@ -48,6 +72,8 @@ def establish_connection(device_config):
|
|||||||
logger.error(f"Failed to establish connection to {device_config['host']}: {e}")
|
logger.error(f"Failed to establish connection to {device_config['host']}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# Execute command on network device
|
||||||
def execute_command(device, command_format, target, ip_version):
|
def execute_command(device, command_format, target, ip_version):
|
||||||
device_config = {
|
device_config = {
|
||||||
'device_type': device['type'],
|
'device_type': device['type'],
|
||||||
|
@ -3,3 +3,4 @@ pyyaml==6.0.2
|
|||||||
netmiko==4.5.0
|
netmiko==4.5.0
|
||||||
werkzeug==3.1.3
|
werkzeug==3.1.3
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
|
requests==2.32.3
|
@ -8,8 +8,9 @@ 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 ?? {},
|
||||||
|
currentDevice: null,
|
||||||
currentCommand: null,
|
currentCommand: null,
|
||||||
showHelp: false,
|
showHelp: false,
|
||||||
showTerms: false,
|
showTerms: false,
|
||||||
@ -23,15 +24,31 @@ const app = Vue.createApp({
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
selectedDevice: {
|
||||||
|
handler(newVal) {
|
||||||
|
this.currentDevice = this.devices[newVal] || null;
|
||||||
|
if (!newVal) {
|
||||||
|
this.resetCommandState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
selectedCommand: {
|
selectedCommand: {
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
this.currentCommand = this.commands.find(cmd => cmd.id === newVal);
|
this.currentCommand = this.commands[newVal] || null;
|
||||||
},
|
},
|
||||||
immediate: true
|
immediate: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
devicesList() {
|
||||||
|
return Object.entries(this.devices).map(([key, device]) => ({
|
||||||
|
key,
|
||||||
|
...device
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
showIpVersionSelector() {
|
showIpVersionSelector() {
|
||||||
if (!this.targetIp) return true;
|
if (!this.targetIp) return true;
|
||||||
|
|
||||||
@ -82,15 +99,8 @@ const app = Vue.createApp({
|
|||||||
document.documentElement.classList[this.isDark ? 'add' : 'remove']('dark');
|
document.documentElement.classList[this.isDark ? 'add' : 'remove']('dark');
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleDevice(device) {
|
toggleDevice(deviceKey) {
|
||||||
const wasDeselected = this.selectedDevice === device;
|
this.selectedDevice = this.selectedDevice === deviceKey ? '' : deviceKey;
|
||||||
this.selectedDevice = wasDeselected ? '' : device;
|
|
||||||
|
|
||||||
if (wasDeselected) {
|
|
||||||
this.resetCommandState();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$nextTick(() => this.$forceUpdate());
|
|
||||||
},
|
},
|
||||||
|
|
||||||
resetCommandState() {
|
resetCommandState() {
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
<div class="mb-12 max-w-6xl mx-auto">
|
<div class="mb-12 max-w-6xl mx-auto">
|
||||||
<div class="mb-6"></div>
|
<div class="mb-6"></div>
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-6 gap-4">
|
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-6 gap-4">
|
||||||
{% for device in devices %}
|
{% for device_key, device in devices.items() %}
|
||||||
<button @click="toggleDevice('{{ device.id }}')"
|
<button @click="toggleDevice('{{ device_key }}')"
|
||||||
:class="[
|
:class="[
|
||||||
'flex flex-row items-center justify-between px-4 py-3 h-24 rounded-lg border-2 transition-all duration-200 ease-in-out transform hover:shadow-md',
|
'flex flex-row items-center justify-between px-4 py-3 h-24 rounded-lg border-2 transition-all duration-200 ease-in-out transform hover:shadow-md',
|
||||||
selectedDevice === '{{ device.id }}'
|
selectedDevice === '{{ device_key }}'
|
||||||
? 'bg-gray-100 dark:bg-gray-800 border-gray-700 dark:border-gray-300 text-gray-700 dark:text-gray-300 shadow-lg scale-[1.02]'
|
? 'bg-gray-100 dark:bg-gray-800 border-gray-700 dark:border-gray-300 text-gray-700 dark:text-gray-300 shadow-lg scale-[1.02]'
|
||||||
: 'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-800 hover:border-gray-500 dark:hover:border-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
: 'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-800 hover:border-gray-500 dark:hover:border-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
]">
|
]">
|
||||||
@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Query Type and Target Selection -->
|
<!-- Query Type Selection -->
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition-all ease-in-out duration-200"
|
enter-active-class="transition-all ease-in-out duration-200"
|
||||||
enter-from-class="opacity-0 translate-y-4"
|
enter-from-class="opacity-0 translate-y-4"
|
||||||
@ -36,7 +36,7 @@
|
|||||||
leave-active-class="transition-all ease-in-out duration-200"
|
leave-active-class="transition-all ease-in-out duration-200"
|
||||||
leave-from-class="opacity-100"
|
leave-from-class="opacity-100"
|
||||||
leave-to-class="opacity-0">
|
leave-to-class="opacity-0">
|
||||||
<div v-if="selectedDevice" class="mb-8 max-w-4xl mx-auto">
|
<div v-if="currentDevice" class="mb-8 max-w-4xl mx-auto">
|
||||||
<div class="mb-6"></div>
|
<div class="mb-6"></div>
|
||||||
<div class="flex flex-col md:flex-row gap-4">
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
<!-- Query Type Dropdown -->
|
<!-- Query Type Dropdown -->
|
||||||
@ -45,7 +45,7 @@
|
|||||||
@click.stop="toggleDropdown"
|
@click.stop="toggleDropdown"
|
||||||
class="relative w-full h-12 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm pl-3 pr-10 text-left cursor-pointer focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500 sm:text-sm transition-all duration-200 ease-in-out hover:border-gray-400 dark:hover:border-gray-600">
|
class="relative w-full h-12 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm pl-3 pr-10 text-left cursor-pointer focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500 sm:text-sm transition-all duration-200 ease-in-out hover:border-gray-400 dark:hover:border-gray-600">
|
||||||
<span class="block truncate text-gray-700 dark:text-gray-300">
|
<span class="block truncate text-gray-700 dark:text-gray-300">
|
||||||
${selectedCommand ? commands.find(c => c.id === selectedCommand).display_name : 'Select query type...'}
|
${currentCommand ? currentCommand.display_name : 'Select query type...'}
|
||||||
</span>
|
</span>
|
||||||
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none text-gray-400">
|
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none text-gray-400">
|
||||||
<svg class="h-5 w-5 transition-transform duration-200"
|
<svg class="h-5 w-5 transition-transform duration-200"
|
||||||
@ -64,14 +64,14 @@
|
|||||||
leave-to-class="transform opacity-0 scale-95 -translate-y-2">
|
leave-to-class="transform opacity-0 scale-95 -translate-y-2">
|
||||||
<div v-if="isOpen"
|
<div v-if="isOpen"
|
||||||
class="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-lg py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
class="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-lg py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||||
{% for command in commands %}
|
{% for command_key, command in commands.items() %}
|
||||||
<div @click.stop.prevent="selectCommand('{{ command.id }}')"
|
<div @click.stop.prevent="selectCommand('{{ command_key }}')"
|
||||||
:class="['cursor-pointer select-none relative py-2 pl-3 pr-9 transition-all duration-200 ease-in-out hover:bg-gray-200 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white',
|
:class="['cursor-pointer select-none relative py-2 pl-3 pr-9 transition-all duration-200 ease-in-out hover:bg-gray-200 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white',
|
||||||
selectedCommand === '{{ command.id }}' ? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white' : 'text-gray-700 dark:text-gray-300']">
|
selectedCommand === '{{ command_key }}' ? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white' : 'text-gray-700 dark:text-gray-300']">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="block truncate font-medium">{{ command.display_name }}</span>
|
<span class="block truncate font-medium">{{ command.display_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="selectedCommand === '{{ command.id }}'"
|
<span v-if="selectedCommand === '{{ command_key }}'"
|
||||||
class="absolute inset-y-0 right-0 flex items-center pr-4 text-gray-600 dark:text-gray-400">
|
class="absolute inset-y-0 right-0 flex items-center pr-4 text-gray-600 dark:text-gray-400">
|
||||||
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||||
@ -91,7 +91,7 @@
|
|||||||
leave-active-class="transition-all ease-in-out duration-200"
|
leave-active-class="transition-all ease-in-out duration-200"
|
||||||
leave-from-class="transform opacity-100 scale-100"
|
leave-from-class="transform opacity-100 scale-100"
|
||||||
leave-to-class="transform opacity-0 scale-95">
|
leave-to-class="transform opacity-0 scale-95">
|
||||||
<div v-if="selectedCommand && currentCommand"
|
<div v-if="currentCommand"
|
||||||
class="flex-1 flex items-center gap-2">
|
class="flex-1 flex items-center gap-2">
|
||||||
<!-- Target Input Row -->
|
<!-- Target Input Row -->
|
||||||
<div class="flex items-center gap-2 w-full">
|
<div class="flex items-center gap-2 w-full">
|
||||||
|
@ -3,7 +3,7 @@ from flask import (
|
|||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from app.functions.utils import load_yaml, execute_command, exception_handler
|
from app.functions.utils import exception_handler, load_yaml, execute_command, send_webhook
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ def index():
|
|||||||
commands = load_yaml('commands')
|
commands = load_yaml('commands')
|
||||||
|
|
||||||
# Remove sensitive data before passing to Jinja as additional security measure
|
# Remove sensitive data before passing to Jinja as additional security measure
|
||||||
for device in devices:
|
for device_key, device in devices.items():
|
||||||
device.pop('username', None)
|
device.pop('username', None)
|
||||||
device.pop('password', None)
|
device.pop('password', None)
|
||||||
device.pop('host', None)
|
device.pop('host', None)
|
||||||
@ -31,32 +31,33 @@ def index():
|
|||||||
@exception_handler
|
@exception_handler
|
||||||
def execute():
|
def execute():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
device = data.get('device')
|
input_device = data.get('device')
|
||||||
command = data.get('command')
|
input_command = data.get('command')
|
||||||
target = data.get('target')
|
input_target = data.get('target').strip()
|
||||||
ip_version = data.get('ipVersion')
|
input_ip_version = data.get('ipVersion')
|
||||||
|
|
||||||
if not all([device, command, target, ip_version]):
|
if not all([input_device, input_command, input_target, input_ip_version]):
|
||||||
raise Exception("Missing required parameters")
|
raise Exception("Missing required parameters")
|
||||||
|
|
||||||
# Load configurations
|
# Load configurations
|
||||||
devices = load_yaml('devices')
|
device = load_yaml('devices', key=input_device)
|
||||||
commands = load_yaml('commands')
|
command = load_yaml('commands', key=input_command)
|
||||||
|
webhook = load_yaml('config', key='webhook')
|
||||||
# Find device and command configurations
|
|
||||||
device = next((d for d in devices if d['id'] == device), None)
|
|
||||||
command = next((c for c in commands if c['id'] == command), None)
|
|
||||||
|
|
||||||
if not device or not command:
|
if not device or not command:
|
||||||
raise Exception("Device or command not found")
|
raise Exception("Device or command not found")
|
||||||
|
|
||||||
# Verify command is allowed for this device
|
# Verify command is allowed for this device
|
||||||
if command['id'] not in device.get('commands', []):
|
if input_command not in device.get('commands', []):
|
||||||
raise Exception("Command not allowed for this device")
|
raise Exception("Command not allowed for this device")
|
||||||
|
|
||||||
ip_version = 6 if ip_version == "IPv6" else 4
|
ip_version = 6 if input_ip_version == "IPv6" else 4
|
||||||
|
|
||||||
# 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'], input_target, ip_version)
|
||||||
|
|
||||||
|
# Send a webhook notification with client IP and command output
|
||||||
|
if not result['error'] and webhook:
|
||||||
|
send_webhook(webhook['url'], f"Client IP: `{request.remote_addr}`\nDevice: `{input_device}`\nCommand: `{input_command} {input_target}`")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
Loading…
x
Reference in New Issue
Block a user