refactor to better handle configuration loading and usage

This commit is contained in:
Micky 2024-12-28 14:12:30 +11:00
parent e2a718a23b
commit e7b0ee08b3
8 changed files with 69 additions and 85 deletions

View File

@ -13,7 +13,7 @@ services:
# - ./instance/images:/app/static/images # Commented out by default to use default logos
```
### instance/config.yaml
### instance/site.yaml
```
header:
title: "photonglass"
@ -23,7 +23,10 @@ footer:
text: "photonglass"
peeringdb_href: "https://www.peeringdb.com"
github_href: "https://github.com/alimickey"
```
### instance/config.yaml
```
webhook:
url: "https://hooks.slack.com/###"
```
@ -62,15 +65,14 @@ sydney1:
subtext: "Equinix SY3"
country_code: "AU"
type: "linux"
commands:
- ping
- traceroute
- mtr
credentials:
host: "IP_ADDRESS"
port: PORT
username: "USERNAME"
password: "PASSWORD"
ssh_key: "id_rsa" # Optional
commands:
- ping
- traceroute
- mtr
```

View File

@ -6,6 +6,7 @@ A modern, distributed looking glass application that provides network insight fo
## Features
- **Multi Device Support**: Connect to multiple devices from one single interface.
- **Custom Command Support**: Built dynamically to support any custom command. Defaults are [ping, traceroute, mtr]
- **Easy Deployment**: Extremely easy to deploy and scale with multiple devices.
- **Webhook Logging**: Log queries to a webhook channel (optional).
- **Rate Limiting**: Reduce service abuse by rate limiting users, 100 requests per day and 10 requests per minute by default.
@ -32,9 +33,9 @@ If you wish to list your instance on this list, please open a Github issue.
- Refer to [CONFIGURATION.md](CONFIGURATION.md)
4. Create `docker-compose.yml`
- Refer to [CONFIGURATION.md](CONFIGURATION.md)
4. Build and deploy the container (inital build may take a minute)
5. Build and deploy the container (inital build may take a minute)
- `docker compose up -d --build`
5. View the app at `http://IP_ADDRESS:5000`, recommend using a reverse proxy (traefik) for production use.
6. View the app at `http://IP_ADDRESS:5000`, recommend using a reverse proxy (traefik) for production use.
## Attribution

View File

@ -1,4 +1,4 @@
import logging
import logging, yaml, os
from flask import Flask
from flask_limiter import Limiter
@ -18,6 +18,22 @@ def create_app():
limiter.init_app(app)
config_files = ['config.yaml', 'site.yaml', 'devices.yaml', 'commands.yaml']
for config_file in config_files:
config_path = os.path.join("/instance", config_file)
# Create empty config files if they don't exist
if not os.path.exists(config_path):
with open(config_path, "w") as f:
pass
# Load the config files into the app config
with open(config_path, 'r') as file:
config_yaml = yaml.safe_load(file) or {}
app.config[config_file.split('.')[0].upper()] = config_yaml
from app.views import main
app.register_blueprint(main.bp)
app.add_url_rule('/', endpoint='index')

View File

@ -18,11 +18,13 @@ def establish_connection(device_config):
# Execute command on network device
def execute_command(device, command_format, target, ip_version):
device_credentials = device['credentials']
device_config = {
'device_type': device['type'],
'host': device['host'],
'port': device['port'],
'username': device['username'],
'host': device_credentials['host'],
'port': device_credentials['port'],
'username': device_credentials['username'],
'timeout': 10,
'session_timeout': 60,
'conn_timeout': 10,
@ -30,22 +32,22 @@ def execute_command(device, command_format, target, ip_version):
}
# Use SSH key if provided
if "ssh_key" in device:
key_path = os.path.join("/instance/ssh-keys", device['ssh_key'])
if "ssh_key" in device_credentials:
key_path = os.path.join("/instance/ssh-keys", device_credentials['ssh_key'])
if not os.path.exists(key_path):
logger.error(f"SSH file not found: {key_path} for {device['host']}")
logger.error(f"SSH file not found: {key_path} for {device_credentials['host']}")
return {'error': True, 'message': 'Authentication failed'}
device_config['use_keys'] = True
device_config['key_file'] = os.path.join("/instance/ssh-keys", device['ssh_key'])
device_config['key_file'] = os.path.join("/instance/ssh-keys", device_credentials['ssh_key'])
else:
device_config['password'] = device['password']
device_config['password'] = device_credentials['password']
command_timeout = 30
try:
command_timeout = 30
with establish_connection(device_config) as connection:
# Format the command
command = str(command_format.format(ip_version=ip_version, target=target).strip())

View File

@ -1,4 +1,4 @@
import os, yaml, logging, requests
import logging, requests
from functools import wraps
from flask import request
@ -18,39 +18,6 @@ def exception_handler(func):
return wrapper
# Load YAML configuration file
@exception_handler
def load_yaml(filename, key=None):
if not filename.endswith('.yaml'):
filename = f"{filename}.yaml"
config_path = os.path.join("/instance", filename)
if not os.path.exists(config_path):
logger.error(f"Configuration file not found: {config_path}")
return {}
try:
with open(config_path, 'r') as f:
data = yaml.safe_load(f)
if data is None:
logger.error(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 {}
except Exception as e:
logger.error(f"Unexpected error loading {filename}: {e}")
return {}
# Send data to a webhook URL
@exception_handler
def send_webhook(webhook_url, text_data):

View File

@ -5,16 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='images/favicon.svg') }}"/>
<link rel="apple-touch-icon" href="{{ url_for('static', filename='images/favicon.svg') }}"/>
<title>{{ config.header.title }}</title>
<title>{{ site.header.title }}</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@6.11.0/css/flag-icons.min.css"/>
<script>
// Initialize window.initialData for Vue
window.initialData = {
config: {{ config|tojson|safe }},
commands: {{ commands|tojson|safe }},
devices: {{ devices|tojson|safe }}
site: {{ site|tojson|safe }},
devices: {{ devices|tojson|safe }},
commands: {{ commands|tojson|safe }}
};
</script>
</head>
@ -23,7 +23,7 @@
<div id="app" class="min-h-screen flex flex-col">
<div class="flex-grow w-full max-w-7xl px-4 mx-auto">
<div class="mt-8 mb-12 flex justify-center items-center">
<a href="{{ config.header.logo_href }}" target="_blank" class="inline-block">
<a href="{{ site.header.logo_href }}" target="_blank" class="inline-block">
<img src="{{ url_for('static', filename='images/logo-light.svg') }}" alt="Logo" class="h-12 w-auto dark:hidden"/>
<img src="{{ url_for('static', filename='images/logo-dark.svg') }}" alt="Logo" class="h-12 w-auto hidden dark:block"/>
</a>

View File

@ -4,7 +4,7 @@
<div class="flex flex-col md:flex-row justify-between items-center space-y-6 md:space-y-0">
<div class="flex items-center">
<span class="text-sm text-gray-600 dark:text-gray-400">
{{ config.footer.text }}
{{ site.footer.text }} - v1.0.0
</span>
</div>
@ -20,12 +20,12 @@
</button>
<!-- PeeringDB Link -->
<a href="{{ config.footer.peeringdb_href }}" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200">
<a href="{{ site.footer.peeringdb_href }}" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200">
PeeringDB
</a>
<!-- GitHub Link -->
<a href="{{ config.footer.github_href }}" target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800">
<a href="{{ site.footer.github_href }}" target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>

View File

@ -1,8 +1,8 @@
import logging
from copy import deepcopy
from flask import Blueprint, request, render_template, current_app
from flask import Blueprint, request, render_template
from app.functions.utils import exception_handler, load_yaml, send_webhook, get_client_ip
from app.functions.utils import exception_handler, send_webhook, get_client_ip
from app.functions.netmiko import execute_command
logger = logging.getLogger(__name__)
@ -15,18 +15,14 @@ bp = Blueprint('main', __name__)
@bp.route('/')
@exception_handler
def index():
config = load_yaml('config')
devices = load_yaml('devices')
commands = load_yaml('commands')
site = current_app.config['SITE']
devices = deepcopy(current_app.config['DEVICES'])
commands = current_app.config['COMMANDS']
# Remove sensitive data before passing to Jinja as additional security measure
for device_key, device in devices.items():
device.pop('username', None)
device.pop('password', None)
device.pop('host', None)
device.pop('port', None)
for device in devices.values():
device.pop('credentials', None)
return render_template('index.html', config=config, devices=devices, commands=commands)
return render_template('index.html', site=site, devices=devices, commands=commands)
# Route to handle command execution requests
@ -34,6 +30,7 @@ def index():
@exception_handler
def execute():
data = request.get_json()
input_device = data.get('device')
input_command = data.get('command')
input_target = data.get('target').strip()
@ -42,11 +39,10 @@ def execute():
if not all([input_device, input_command, input_target, input_ip_version]):
raise Exception("Missing required parameters")
# Load configurations
device = load_yaml('devices', key=input_device)
command = load_yaml('commands', key=input_command)
webhook = load_yaml('config', key='webhook')
device = current_app.config['DEVICES'].get(input_device, {})
command = current_app.config['COMMANDS'].get(input_command, {})
# Verify device and command exist
if not device or not command:
raise Exception("Device or command not found")
@ -54,12 +50,12 @@ def execute():
if input_command not in device.get('commands', []):
raise Exception("Command not allowed for this device")
ip_version = 6 if input_ip_version == "IPv6" else 4
# Execute the command using network_utils
ip_version = 6 if input_ip_version == "IPv6" else 4
result = execute_command(device, command['format'], input_target, ip_version)
# Send a webhook notification with client IP and command output
webhook = current_app.config['CONFIG'].get('webhook')
if not result['error'] and webhook:
client_ip = get_client_ip()
send_webhook(webhook['url'], f"Client IP: `{client_ip}`\nDevice: `{input_device}`\nCommand: `{input_command} -{ip_version} {input_target}`")