Initial commit

This commit is contained in:
2026-05-05 19:57:47 +00:00
commit 000ba27387
7 changed files with 1604 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
.env
__pycache__/
*.pyc
config.py
*.bak
configs/
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
COPY config.py .
COPY templates/ templates/
EXPOSE 8003
CMD ["python", "app.py"]
+147
View File
@@ -0,0 +1,147 @@
from flask import Flask, jsonify, request, render_template, send_from_directory
import requests
import os
import json
import time
import socket
import paramiko
from config import NOCODB_URL, NOCODB_TOKEN, NOCODB_BASE_ID, NOCODB_TABLE_ID, CONFIGS_DIR
app = Flask(__name__)
app.config['TEMPLATES_AUTO_RELOAD'] = True
os.makedirs(CONFIGS_DIR, exist_ok=True)
# ─── NocoDB ───────────────────────────────────────────────────────────────────
def get_switches(dept=None):
headers = {"xc-token": NOCODB_TOKEN}
params = {
"where": "(Active,eq,YES)",
"limit": 500,
"sort": "Hostname"
}
if dept:
params["where"] = f"(Active,eq,YES)~and(Dept,eq,{dept})"
url = f"{NOCODB_URL}/api/v1/db/data/noco/{NOCODB_BASE_ID}/{NOCODB_TABLE_ID}"
resp = requests.get(url, headers=headers, params=params, timeout=10)
resp.raise_for_status()
return resp.json().get("list", [])
# ─── Routes ───────────────────────────────────────────────────────────────────
@app.route("/")
def index():
return render_template("index.html")
@app.route("/api/switches")
def api_switches():
try:
switches = get_switches()
return jsonify({"ok": True, "switches": switches})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/save", methods=["POST"])
def api_save():
data = request.get_json()
hostname = data.get("hostname", "").strip()
content = data.get("content", "")
if not hostname:
return jsonify({"ok": False, "error": "No hostname provided"}), 400
filename = f"{hostname}.conf"
filepath = os.path.join(CONFIGS_DIR, filename)
# Check if file already exists
exists = os.path.isfile(filepath)
if exists and not data.get("overwrite", False):
return jsonify({"ok": False, "exists": True, "filename": filename}), 409
try:
with open(filepath, "w") as f:
f.write(content)
return jsonify({"ok": True, "filename": filename, "path": filepath})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/configs")
def api_configs():
try:
files = sorted([
f for f in os.listdir(CONFIGS_DIR) if f.endswith(".conf")
])
return jsonify({"ok": True, "files": files})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/web-access", methods=["POST"])
def api_web_access():
data = request.get_json()
ip = data.get("ip", "").strip()
username = data.get("username", "").strip()
password = data.get("password", "").strip()
action = data.get("action", "") # "enable" or "disable"
if not ip or not username or not password:
return jsonify({"ok": False, "error": "Missing IP or credentials"}), 400
if action not in ("enable", "disable"):
return jsonify({"ok": False, "error": "Invalid action"}), 400
print(f"[web-access] {action} {ip} user={username!r} pass_len={len(password)}", flush=True)
cmd = "ip http secure-server" if action == "enable" else "no ip http secure-server"
try:
sock = socket.create_connection((ip, 22), timeout=10)
transport = paramiko.Transport(sock)
transport.start_client(timeout=10)
# Try password auth.
# BadAuthenticationType means the server disallows password auth entirely
# (some FS switches require keyboard-interactive instead).
# A plain AuthenticationException means the credentials were rejected — don't
# fall back to keyboard-interactive in that case.
try:
transport.auth_password(username, password)
except paramiko.ssh_exception.BadAuthenticationType as e:
if "keyboard-interactive" in e.allowed_types:
transport.auth_interactive_dumb(username, [password])
else:
raise paramiko.AuthenticationException(
f"Password auth not accepted; server allows: {e.allowed_types}"
)
shell = transport.open_session()
shell.get_pty()
shell.invoke_shell()
time.sleep(1)
shell.recv(65535) # drain login banner
for command in ["conf t", cmd, "end"]:
shell.send(command + "\n")
time.sleep(0.5)
# copy run start may prompt for filename — send \n to confirm default
shell.send("copy run start\n")
time.sleep(0.3)
shell.send("\n")
time.sleep(1.5)
output = shell.recv(65535).decode("utf-8", errors="ignore") if shell.recv_ready() else ""
transport.close()
return jsonify({"ok": True, "output": output})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8003, debug=False)
+11
View File
@@ -0,0 +1,11 @@
# config.py - Switch Config Manager
# Copy this file to config.py and fill in your values.
# ─── NocoDB ───────────────────────────────────────────────────────────────────
NOCODB_URL = "http://your-nocodb-host:8080"
NOCODB_TOKEN = "your-nocodb-api-token"
NOCODB_BASE_ID = "your-base-id"
NOCODB_TABLE_ID = "your-table-id"
# ─── Output ───────────────────────────────────────────────────────────────────
CONFIGS_DIR = "/configs"
+14
View File
@@ -0,0 +1,14 @@
services:
switch-config-manager:
build: .
container_name: switch-config-manager
restart: unless-stopped
ports:
- "8003:8003"
volumes:
- ./config.py:/app/config.py
- ./templates/index.html:/app/templates/index.html
- ./configs:/configs
environment:
- PYTHONUNBUFFERED=1
+3
View File
@@ -0,0 +1,3 @@
flask==3.0.3
requests==2.31.0
paramiko==3.4.0
+1409
View File
File diff suppressed because it is too large Load Diff