refactor to better handle configuration loading and usage
This commit is contained in:
parent
e2a718a23b
commit
e7b0ee08b3
@ -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
|
||||
```
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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())
|
||||
|
@ -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):
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}`")
|
||||
|
Loading…
x
Reference in New Issue
Block a user