From d2dd1525f5cf62b060270002bf594f83d7afceaf Mon Sep 17 00:00:00 2001 From: Micky <60691199+AliMickey@users.noreply.github.com> Date: Thu, 26 Dec 2024 12:37:29 +1100 Subject: [PATCH] more cleanup, add webhook support --- app/functions/utils.py | 30 ++++++++++++++++++++++++++++-- app/requirements.txt | 1 + app/static/js/main.js | 34 ++++++++++++++++++++++------------ app/templates/index.html | 22 +++++++++++----------- app/views/main.py | 33 +++++++++++++++++---------------- 5 files changed, 79 insertions(+), 41 deletions(-) diff --git a/app/functions/utils.py b/app/functions/utils.py index 88ac041..ce58b7e 100644 --- a/app/functions/utils.py +++ b/app/functions/utils.py @@ -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'], diff --git a/app/requirements.txt b/app/requirements.txt index 9e77704..e4d4d67 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -3,3 +3,4 @@ pyyaml==6.0.2 netmiko==4.5.0 werkzeug==3.1.3 gunicorn==23.0.0 +requests==2.32.3 \ No newline at end of file diff --git a/app/static/js/main.js b/app/static/js/main.js index 687fa40..b48dfdc 100644 --- a/app/static/js/main.js +++ b/app/static/js/main.js @@ -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() { diff --git a/app/templates/index.html b/app/templates/index.html index 5d798fe..cb8f2c1 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -6,11 +6,11 @@
- {% for device in devices %} -
- + -
+
@@ -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"> - ${selectedCommand ? commands.find(c => c.id === selectedCommand).display_name : 'Select query type...'} + ${currentCommand ? currentCommand.display_name : 'Select query type...'}
- {% for command in commands %} -
+ selectedCommand === '{{ command_key }}' ? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white' : 'text-gray-700 dark:text-gray-300']">
{{ command.display_name }}
- @@ -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"> -
diff --git a/app/views/main.py b/app/views/main.py index 3aed343..ddbe226 100644 --- a/app/views/main.py +++ b/app/views/main.py @@ -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