Initial commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
.env
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
config.py
|
||||||
|
*.bak
|
||||||
|
configs/
|
||||||
+14
@@ -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"]
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
flask==3.0.3
|
||||||
|
requests==2.31.0
|
||||||
|
paramiko==3.4.0
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user