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 netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Exception handler
|
||||
def exception_handler(func):
|
||||
@wraps(func)
|
||||
@ -15,8 +16,10 @@ def exception_handler(func):
|
||||
return "An error occurred", 500
|
||||
return wrapper
|
||||
|
||||
|
||||
# Load YAML configuration file
|
||||
@exception_handler
|
||||
def load_yaml(filename):
|
||||
def load_yaml(filename, key=None):
|
||||
if not filename.endswith('.yaml'):
|
||||
filename = f"{filename}.yaml"
|
||||
|
||||
@ -32,7 +35,13 @@ def load_yaml(filename):
|
||||
if data is None:
|
||||
logger.warning(f"Empty configuration file: {filename}")
|
||||
return {}
|
||||
|
||||
# Return specific key if provided
|
||||
if key:
|
||||
return data.get(key, None)
|
||||
|
||||
return data
|
||||
|
||||
except yaml.YAMLError as e:
|
||||
logger.error(f"YAML parsing error in {filename}: {e}")
|
||||
return {}
|
||||
@ -41,6 +50,21 @@ def load_yaml(filename):
|
||||
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):
|
||||
try:
|
||||
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}")
|
||||
raise
|
||||
|
||||
|
||||
# Execute command on network device
|
||||
def execute_command(device, command_format, target, ip_version):
|
||||
device_config = {
|
||||
'device_type': device['type'],
|
||||
|
@ -3,3 +3,4 @@ pyyaml==6.0.2
|
||||
netmiko==4.5.0
|
||||
werkzeug==3.1.3
|
||||
gunicorn==23.0.0
|
||||
requests==2.32.3
|
@ -8,8 +8,9 @@ const app = Vue.createApp({
|
||||
selectedIpVersion: 'IPv4',
|
||||
isLoading: false,
|
||||
commandResult: '',
|
||||
devices: window.initialData?.devices ?? [],
|
||||
commands: window.initialData?.commands ?? [],
|
||||
devices: window.initialData?.devices ?? {},
|
||||
commands: window.initialData?.commands ?? {},
|
||||
currentDevice: null,
|
||||
currentCommand: null,
|
||||
showHelp: false,
|
||||
showTerms: false,
|
||||
@ -23,15 +24,31 @@ const app = Vue.createApp({
|
||||
},
|
||||
|
||||
watch: {
|
||||
selectedDevice: {
|
||||
handler(newVal) {
|
||||
this.currentDevice = this.devices[newVal] || null;
|
||||
if (!newVal) {
|
||||
this.resetCommandState();
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
selectedCommand: {
|
||||
handler(newVal) {
|
||||
this.currentCommand = this.commands.find(cmd => cmd.id === newVal);
|
||||
this.currentCommand = this.commands[newVal] || null;
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
devicesList() {
|
||||
return Object.entries(this.devices).map(([key, device]) => ({
|
||||
key,
|
||||
...device
|
||||
}));
|
||||
},
|
||||
|
||||
showIpVersionSelector() {
|
||||
if (!this.targetIp) return true;
|
||||
|
||||
@ -82,15 +99,8 @@ const app = Vue.createApp({
|
||||
document.documentElement.classList[this.isDark ? 'add' : 'remove']('dark');
|
||||
},
|
||||
|
||||
toggleDevice(device) {
|
||||
const wasDeselected = this.selectedDevice === device;
|
||||
this.selectedDevice = wasDeselected ? '' : device;
|
||||
|
||||
if (wasDeselected) {
|
||||
this.resetCommandState();
|
||||
}
|
||||
|
||||
this.$nextTick(() => this.$forceUpdate());
|
||||
toggleDevice(deviceKey) {
|
||||
this.selectedDevice = this.selectedDevice === deviceKey ? '' : deviceKey;
|
||||
},
|
||||
|
||||
resetCommandState() {
|
||||
|
@ -6,11 +6,11 @@
|
||||
<div class="mb-12 max-w-6xl mx-auto">
|
||||
<div class="mb-6"></div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-6 gap-4">
|
||||
{% for device in devices %}
|
||||
<button @click="toggleDevice('{{ device.id }}')"
|
||||
{% for device_key, device in devices.items() %}
|
||||
<button @click="toggleDevice('{{ device_key }}')"
|
||||
: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',
|
||||
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-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>
|
||||
|
||||
<!-- Query Type and Target Selection -->
|
||||
<!-- Query Type Selection -->
|
||||
<transition
|
||||
enter-active-class="transition-all ease-in-out duration-200"
|
||||
enter-from-class="opacity-0 translate-y-4"
|
||||
@ -36,7 +36,7 @@
|
||||
leave-active-class="transition-all ease-in-out duration-200"
|
||||
leave-from-class="opacity-100"
|
||||
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="flex flex-col md:flex-row gap-4">
|
||||
<!-- Query Type Dropdown -->
|
||||
@ -45,7 +45,7 @@
|
||||
@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">
|
||||
<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 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"
|
||||
@ -64,14 +64,14 @@
|
||||
leave-to-class="transform opacity-0 scale-95 -translate-y-2">
|
||||
<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">
|
||||
{% for command in commands %}
|
||||
<div @click.stop.prevent="selectCommand('{{ command.id }}')"
|
||||
{% for command_key, command in commands.items() %}
|
||||
<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',
|
||||
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">
|
||||
<span class="block truncate font-medium">{{ command.display_name }}</span>
|
||||
</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">
|
||||
<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" />
|
||||
@ -91,7 +91,7 @@
|
||||
leave-active-class="transition-all ease-in-out duration-200"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
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">
|
||||
<!-- Target Input Row -->
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
|
@ -3,7 +3,7 @@ from flask import (
|
||||
)
|
||||
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__)
|
||||
|
||||
@ -17,7 +17,7 @@ def index():
|
||||
commands = load_yaml('commands')
|
||||
|
||||
# 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('password', None)
|
||||
device.pop('host', None)
|
||||
@ -31,32 +31,33 @@ def index():
|
||||
@exception_handler
|
||||
def execute():
|
||||
data = request.get_json()
|
||||
device = data.get('device')
|
||||
command = data.get('command')
|
||||
target = data.get('target')
|
||||
ip_version = data.get('ipVersion')
|
||||
input_device = data.get('device')
|
||||
input_command = data.get('command')
|
||||
input_target = data.get('target').strip()
|
||||
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")
|
||||
|
||||
# Load configurations
|
||||
devices = load_yaml('devices')
|
||||
commands = load_yaml('commands')
|
||||
|
||||
# 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)
|
||||
device = load_yaml('devices', key=input_device)
|
||||
command = load_yaml('commands', key=input_command)
|
||||
webhook = load_yaml('config', key='webhook')
|
||||
|
||||
if not device or not command:
|
||||
raise Exception("Device or command not found")
|
||||
|
||||
# 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")
|
||||
|
||||
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
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user