more cleanup, add webhook support

This commit is contained in:
Micky 2024-12-26 12:37:29 +11:00
parent a7d23846e6
commit d2dd1525f5
5 changed files with 79 additions and 41 deletions

View File

@ -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'],

View File

@ -3,3 +3,4 @@ pyyaml==6.0.2
netmiko==4.5.0
werkzeug==3.1.3
gunicorn==23.0.0
requests==2.32.3

View File

@ -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() {

View File

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

View File

@ -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