Add files via upload
This commit is contained in:
+1456
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
|
||||
# Generated by Avaya_5952_setup.py
|
||||
# {fqdn} is replaced with your chosen hostname
|
||||
{fqdn} {{
|
||||
reverse_proxy switch-manager:8765
|
||||
tls internal
|
||||
}}
|
||||
|
||||
:80 {{
|
||||
redir https://{{host}}{{uri}} permanent
|
||||
}}
|
||||
@@ -0,0 +1,8 @@
|
||||
\
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
RUN pip install --no-cache-dir fastapi uvicorn paramiko pyotp
|
||||
COPY switch_backend.py .
|
||||
COPY frontend/dist ./frontend/dist
|
||||
EXPOSE 8765
|
||||
CMD ["uvicorn", "switch_backend:app", "--host", "0.0.0.0", "--port", "8765"]
|
||||
+402
@@ -0,0 +1,402 @@
|
||||
# Avaya ERS 5952 Switch Manager — Project Handoff
|
||||
|
||||
## What This Is
|
||||
|
||||
A browser-based management interface for the Avaya / Extreme Networks ERS 5952
|
||||
enterprise switch. The switch has no REST API — everything works via SSH
|
||||
screen-scraping: the backend opens SSH sessions, sends CLI commands one at a time,
|
||||
checks each response for errors, and stops on first failure. Config is only saved
|
||||
to NVRAM if every command succeeds.
|
||||
|
||||
The entire project ships as a single Python file: `Avaya_5952_setup.py`
|
||||
This file contains three embedded Python string constants:
|
||||
- BACKEND_SRC — the FastAPI backend (Python, ~1800 lines)
|
||||
- JSX_SRC — the React frontend (JSX, ~2600 lines)
|
||||
- README_SRC — the README (Markdown, ~400 lines)
|
||||
|
||||
Running `python Avaya_5952_setup.py` extracts and runs 21 setup steps that
|
||||
install everything, configure SSH keys, set up TOTP, build the React app,
|
||||
install Docker + Caddy or native systemd, set up WireGuard VPN, and optionally
|
||||
install Control D DNS filtering via ctrld.
|
||||
|
||||
The attached file IS the project. Everything is in it.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser (any device on VLAN 99 or WireGuard VPN)
|
||||
↓ https://switch.mgmt.lan (Caddy) or http://IP:8765
|
||||
FastAPI backend (Python, runs on management computer)
|
||||
↓ ed25519 SSH key (never leaves management computer)
|
||||
ERS 5952 switch (192.168.99.1)
|
||||
```
|
||||
|
||||
Management computer = Raspberry Pi, HP T620 thin client, or any always-on Linux box.
|
||||
Runs Docker (recommended) with Caddy for HTTPS, or native Python with systemd.
|
||||
|
||||
---
|
||||
|
||||
## Current State — What Is Built and Working
|
||||
|
||||
### Setup script (21 steps)
|
||||
1. Writes project files from embedded strings
|
||||
2. Installs system packages (picocom, qrencode, wireguard-tools)
|
||||
3. Adds user to dialout group (serial port access)
|
||||
4. Installs Python dependencies (fastapi, uvicorn, paramiko, pyotp)
|
||||
5. Creates /etc/switch-manager/ config directory
|
||||
6. Interactive static IP setter (detects interfaces, netplan or interfaces file)
|
||||
7. Docker vs native Python choice — explained before asking, Docker is default
|
||||
8. Console cable guidance (OS-specific, software download links)
|
||||
9. SSH keypair generation (ed25519)
|
||||
10. Loads public key onto switch (console step — only manual step)
|
||||
11. SSH connectivity test with retry
|
||||
12. Pins switch host key (MITM protection)
|
||||
13. TOTP setup (qrencode or Python qrcode, terminal QR display, verification)
|
||||
14. Patches backend config with switch IP/port/user
|
||||
15. Builds React frontend (Vite)
|
||||
16. Docker setup (official installer, docker group, compose plugin, Dockerfile,
|
||||
docker-compose.yml with Caddy, Caddyfile, CA cert installation)
|
||||
OR native systemd service
|
||||
17-19. Start service
|
||||
20. WireGuard VPN (local server fully automated, OPNsense semi-automated,
|
||||
per-client QR codes and .conf files, split tunnel)
|
||||
21. Control D ctrld DNS filtering (all 3 options with explanations)
|
||||
|
||||
### UI — 8 tabs
|
||||
- **Port Map** — visual 48+4 chassis, colour-coded by VLAN, live status,
|
||||
click-to-configure panel (mode/VLAN/PoE), connection banner
|
||||
- **VLANs** — create/rename/delete, port counts
|
||||
- **ACL Builder** — visual rule builder (action/proto/src/dst/port),
|
||||
assigned to VLAN interface with direction
|
||||
- **Review & Push** — annotated CLI table (every command with plain-English
|
||||
explanation), batch or step-by-step push, danger check, TOTP gate,
|
||||
per-command results with switch error output
|
||||
- **Device Access** — MAC randomization warnings (iOS/Android/macOS/Windows),
|
||||
DHCP reservation push, ACL pinhole push, live DHCP lease discovery
|
||||
- **DHCP** — unified switch + OPNsense view, conflict detection (same MAC in
|
||||
both places), one-click sync in 4 directions, OPNsense API integration
|
||||
- **DNS Filtering** — Control D ctrld setup, all 3 options with full
|
||||
explanations, per-VLAN Resolver IDs, ctrld.toml generation, DoH3 upstreams,
|
||||
source-IP-based VLAN routing
|
||||
- **VPN** — WireGuard peer management, add clients (QR + .conf), revoke,
|
||||
live peer status, SSH tunnel documentation
|
||||
|
||||
### Security model
|
||||
- VLAN 99 isolation — management network unreachable from other VLANs
|
||||
- ed25519 SSH key on management computer only — never transmitted
|
||||
- TOTP gates every push session — one code, one push batch, auto-locks
|
||||
- Hard-blocked commands (removing VLAN 99, disabling SSH) refused entirely
|
||||
- Warning-level commands shown for review before push
|
||||
- Per-command execution — stops on first switch error
|
||||
- Config saved to NVRAM only on full success
|
||||
- Connection pool (25s lifetime) — respects switch idle timeout
|
||||
|
||||
### API — 34 endpoints
|
||||
Auth: /api/auth/verify, /api/auth/check, /api/auth/revoke
|
||||
Switch: /api/switch/push, /api/switch/port, /api/switch/vlan (GET/POST/DELETE),
|
||||
/api/switch/acl, /api/switch/config
|
||||
Live: /api/status, /api/live, /api/heartbeat, /api/check/danger
|
||||
Devices: /api/devices, /api/devices/save, /api/devices/delete,
|
||||
/api/devices/push-reservation, /api/devices/push-pinhole
|
||||
DHCP: /api/dhcp/overview, /api/dhcp/detect-opnsense,
|
||||
/api/dhcp/configure-opnsense (POST/DELETE),
|
||||
/api/dhcp/push-to-opnsense, /api/dhcp/sync
|
||||
WireGuard: /api/wireguard/status, /api/wireguard/clients,
|
||||
/api/wireguard/add-client, /api/wireguard/revoke-client,
|
||||
/api/wireguard/client-qr/{name}
|
||||
ctrld: /api/ctrld/status, /api/ctrld/toml-preview, /api/ctrld/save-config,
|
||||
/api/ctrld/update-profiles, /api/ctrld/uninstall
|
||||
|
||||
---
|
||||
|
||||
## What Still Needs to Be Built
|
||||
|
||||
### 1. Port 53 conflict resolution (IMPORTANT — blocks ctrld from working)
|
||||
|
||||
On Linux (Ubuntu/Debian), systemd-resolved holds port 53.
|
||||
Fix: disable the stub listener, not the service itself.
|
||||
|
||||
```bash
|
||||
# Add to /etc/systemd/resolved.conf
|
||||
[Resolve]
|
||||
DNSStubListener=no
|
||||
# Then: sudo systemctl restart systemd-resolved
|
||||
```
|
||||
|
||||
The setup script should detect this and fix it automatically before starting ctrld
|
||||
(Option A — local install). Add to _ctrld_install_local_setup() in the logic section.
|
||||
|
||||
On OPNsense (FreeBSD), Unbound holds port 53.
|
||||
The RIGHT approach (Option C in ctrld terms, not our Option C):
|
||||
- Keep Unbound running — it handles .lan hostnames and local DNS
|
||||
- Move Unbound to listen on 127.0.0.1:5353 instead of :53
|
||||
- Run ctrld on :53
|
||||
- Configure ctrld to forward *.lan, *.local queries to 127.0.0.1:5353
|
||||
- ctrld handles all other queries via DoH3 to Control D
|
||||
|
||||
The ctrld.toml for this split-horizon setup:
|
||||
|
||||
```toml
|
||||
[service]
|
||||
name = "ctrld"
|
||||
|
||||
[[listener]]
|
||||
ip = "0.0.0.0"
|
||||
port = 53
|
||||
tag = "all-vlans"
|
||||
|
||||
# Forward internal names to local Unbound
|
||||
[[upstream]]
|
||||
id = "local"
|
||||
type = "doh" # actually plain DNS to local resolver
|
||||
endpoint = "sdns://AAAA..." # or just use type="legacy" with bootstrap
|
||||
tag = "local"
|
||||
|
||||
# Per-VLAN upstreams
|
||||
[[upstream]]
|
||||
id = "vlan10"
|
||||
type = "doh3"
|
||||
endpoint = "https://dns.controld.com/RESOLVER_ID_VLAN10"
|
||||
tag = "vlan10"
|
||||
|
||||
# ... etc per VLAN ...
|
||||
|
||||
# Route internal domains to local resolver
|
||||
[[rule]]
|
||||
listener = "all-vlans"
|
||||
match_domains = ["lan", "local", "mgmt.lan"]
|
||||
upstream = "local"
|
||||
|
||||
# Route each VLAN to its profile
|
||||
[[rule]]
|
||||
listener = "all-vlans"
|
||||
source_ip = "192.168.10.0/24"
|
||||
upstream = "vlan10"
|
||||
```
|
||||
|
||||
The DNS tab and setup script need to:
|
||||
a) Detect systemd-resolved conflict and fix it (Linux)
|
||||
b) Show OPNsense-specific instructions for moving Unbound to port 5353
|
||||
c) Offer to add local hostname forwarding rules to ctrld.toml
|
||||
d) Ask for the local domain (default: lan) to forward internally
|
||||
|
||||
### 2. DNS enforcement ACLs (IMPORTANT — without this ctrld is bypassable)
|
||||
|
||||
Devices can ignore DHCP-assigned DNS and use 8.8.8.8 directly.
|
||||
The 5952 must block this via ACLs on each VLAN interface.
|
||||
|
||||
For each VLAN (except VLAN 99):
|
||||
```
|
||||
ip access-list extended DNS-ENFORCE-VLAN10
|
||||
permit udp 192.168.10.0 0.0.0.255 host [ctrld-ip] eq 53
|
||||
permit tcp 192.168.10.0 0.0.0.255 host [ctrld-ip] eq 53
|
||||
deny udp 192.168.10.0 0.0.0.255 any eq 53
|
||||
deny tcp 192.168.10.0 0.0.0.255 any eq 53
|
||||
deny tcp 192.168.10.0 0.0.0.255 any eq 853
|
||||
permit ip any any
|
||||
interface vlan 10
|
||||
ip access-group DNS-ENFORCE-VLAN10 in
|
||||
```
|
||||
|
||||
The DNS tab should have a "Enforce DNS on Switch" button that:
|
||||
- Takes the ctrld IP (from status or manual entry)
|
||||
- Generates these ACLs for all configured VLANs
|
||||
- Shows them for review
|
||||
- Pushes via existing TOTP-gated push mechanism
|
||||
|
||||
### 3. Inter-VLAN routing ACL templates
|
||||
|
||||
Pre-built templates in the ACL Builder tab. Currently the ACL builder
|
||||
is blank — user builds rules manually. Add a "Templates" section:
|
||||
|
||||
Templates to build (parameterised by VLAN subnets):
|
||||
|
||||
**Staff VLAN (full access except management):**
|
||||
```
|
||||
deny ip [staff-subnet] 192.168.99.0/24 ← no management access
|
||||
permit ip any any ← everything else allowed
|
||||
```
|
||||
|
||||
**IoT VLAN (internet only):**
|
||||
```
|
||||
deny ip [iot-subnet] 192.168.0.0/16 ← no RFC1918
|
||||
deny ip [iot-subnet] 10.0.0.0/8
|
||||
deny ip [iot-subnet] 172.16.0.0/12
|
||||
permit ip any any ← internet allowed
|
||||
```
|
||||
|
||||
**Guest VLAN (internet only, stricter):**
|
||||
```
|
||||
permit udp [guest-subnet] host [ctrld-ip] eq 53 ← DNS must work first
|
||||
deny ip [guest-subnet] 192.168.0.0/16
|
||||
deny ip [guest-subnet] 10.0.0.0/8
|
||||
deny ip [guest-subnet] 172.16.0.0/12
|
||||
permit ip any any
|
||||
```
|
||||
|
||||
**Camera VLAN (NVR only):**
|
||||
```
|
||||
permit ip [camera-subnet] host [nvr-ip] ← only NVR
|
||||
deny ip [camera-subnet] any ← nothing else
|
||||
```
|
||||
|
||||
**DNS enforcement (per VLAN, as above)**
|
||||
|
||||
The UI should show a "Templates" dropdown in the ACL Builder that
|
||||
pre-fills the rule table for common patterns, then lets the user
|
||||
adjust IPs before pushing.
|
||||
|
||||
### 4. Local hostname resolution (optional but useful)
|
||||
|
||||
Add dnsmasq as an optional Docker service for .lan resolution.
|
||||
Minimal config:
|
||||
|
||||
```
|
||||
# docker-compose.yml addition
|
||||
dnsmasq:
|
||||
image: andyshinn/dnsmasq:latest
|
||||
ports:
|
||||
- "5353:53/udp"
|
||||
- "5353:53/tcp"
|
||||
volumes:
|
||||
- ./dnsmasq.conf:/etc/dnsmasq.conf
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
```
|
||||
|
||||
```
|
||||
# dnsmasq.conf (generated by setup)
|
||||
address=/switch.mgmt.lan/192.168.99.50
|
||||
address=/management.lan/192.168.99.50
|
||||
# Add more as needed
|
||||
```
|
||||
|
||||
ctrld.toml then forwards *.lan to 127.0.0.1:5353 (dnsmasq).
|
||||
|
||||
The DNS tab should have a "Local Hostnames" section where the user
|
||||
adds name → IP mappings. The site generates the dnsmasq.conf and
|
||||
updates ctrld.toml split-horizon rules.
|
||||
|
||||
### 5. Comments pass
|
||||
|
||||
The backend (BACKEND_SRC) needs docstrings and inline comments.
|
||||
Every function needs a one-line docstring. Non-obvious ACL patterns,
|
||||
the connection pool logic, session management, and the ctrld.toml
|
||||
builder all need explanatory comments. Currently comment density
|
||||
is inconsistent.
|
||||
|
||||
### 6. README updates needed
|
||||
|
||||
Add sections for:
|
||||
- Port 53 conflict resolution (systemd-resolved and OPNsense/Unbound)
|
||||
- ctrld on OPNsense vs management computer — tradeoffs
|
||||
- DNS enforcement ACLs — why they matter, what they do
|
||||
- Inter-VLAN routing — the ACL templates, what each VLAN can reach
|
||||
- Local hostname resolution — the dnsmasq option
|
||||
- ACL templates in the UI
|
||||
|
||||
---
|
||||
|
||||
## Key Files and Paths
|
||||
|
||||
All sensitive files in /etc/switch-manager/ (chmod 600 where appropriate):
|
||||
- ers5952_key — SSH private key
|
||||
- ers5952_key.pub — SSH public key (loaded on switch)
|
||||
- known_hosts — pinned switch host key
|
||||
- totp_secret — TOTP seed (BACK THIS UP)
|
||||
- devices.json — registered devices
|
||||
- opnsense.json — OPNsense API credentials
|
||||
- ctrld.json — Control D / ctrld config
|
||||
- wg_server_private — WireGuard server private key
|
||||
- wg_server_public — WireGuard server public key
|
||||
- clients/ — WireGuard client .conf files
|
||||
- fqdn — Caddy hostname (e.g. switch.mgmt.lan)
|
||||
|
||||
Project folder (same dir as Avaya_5952_setup.py):
|
||||
- switch_backend.py — FastAPI backend (written by setup)
|
||||
- ers5952-manager.jsx — React source (written by setup)
|
||||
- README.md — this README (written by setup)
|
||||
- Dockerfile — Docker image
|
||||
- docker-compose.yml — Caddy + switch-manager services
|
||||
- Caddyfile — HTTPS reverse proxy config
|
||||
- ctrld.toml — ctrld per-VLAN config (if configured)
|
||||
- frontend/dist/ — built React app
|
||||
|
||||
---
|
||||
|
||||
## How to Extract/Rebuild the Embedded Files
|
||||
|
||||
```python
|
||||
# Extract any embedded constant from Avaya_5952_setup.py
|
||||
def extract(src, name):
|
||||
idx = src.find(f'\n{name} = ')
|
||||
start = idx + len(f'\n{name} = ')
|
||||
qc = src[start]
|
||||
end = start + 1
|
||||
while end < len(src):
|
||||
if src[end] == qc and src[end-1] != '\\': end += 1; break
|
||||
end += 1
|
||||
return eval(src[start:end])
|
||||
|
||||
setup = open('Avaya_5952_setup.py').read()
|
||||
backend = extract(setup, 'BACKEND_SRC')
|
||||
jsx = extract(setup, 'JSX_SRC')
|
||||
readme = extract(setup, 'README_SRC')
|
||||
|
||||
# Rebuild after modifying
|
||||
header_end = setup.find('\nBACKEND_SRC = ')
|
||||
logic_start = setup.find('\n# ═══\n# SETUP STEPS')
|
||||
header = setup[:header_end]
|
||||
logic = setup[logic_start:]
|
||||
|
||||
new_file = (
|
||||
header
|
||||
+ '\nBACKEND_SRC = ' + repr(backend) + '\n\n'
|
||||
+ 'JSX_SRC = ' + repr(jsx) + '\n\n'
|
||||
+ 'README_SRC = ' + repr(readme) + '\n'
|
||||
+ logic
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design Principles (carry these forward)
|
||||
|
||||
1. **Never write CLI** — user clicks, tool generates, user inspects, user pushes
|
||||
2. **Explain before asking** — every choice is explained before the prompt
|
||||
3. **One file** — Avaya_5952_setup.py contains everything, no other files to copy
|
||||
4. **Stop on first error** — per-command execution, never push blindly
|
||||
5. **Hard block lethal commands** — removing VLAN 99, disabling SSH = refused, run at console
|
||||
6. **Session-scoped auth** — TOTP unlocks one push batch, auto-locks after
|
||||
7. **Self-healing** — per-poll SSH connections, connection pool with liveness check
|
||||
8. **Docker first** — explained as recommended, bare metal available with reasons
|
||||
9. **No OPNsense creep** — OPNsense integration is optional and additive, never required
|
||||
10. **Pi references** — always "management computer (Raspberry Pi, HP T620 thin client,
|
||||
old computer or laptop)" on first mention, then just "management computer"
|
||||
|
||||
---
|
||||
|
||||
## Known Issues / Gotchas
|
||||
|
||||
- The ctrld.toml "local" upstream type needs verification against actual ctrld docs
|
||||
for plain DNS forwarding syntax. Use `type = "legacy"` with `bootstrap_ip` for
|
||||
forwarding to a plain DNS server (Unbound/dnsmasq on localhost).
|
||||
|
||||
- WireGuard `wg syncconf` command in the add-client endpoint may not work on all
|
||||
systems. Fallback: `systemctl reload wg-quick@wg0`. Both should be tried.
|
||||
|
||||
- The OPNsense DHCP API endpoint changed between OPNsense versions. The current
|
||||
code uses `dhcpv4/leases/searchReservation` which is correct for OPNsense 23+.
|
||||
Older versions use a different path.
|
||||
|
||||
- Docker compose file uses named volumes for Caddy data. The Caddy CA cert
|
||||
extraction in _install_caddy_ca() uses `docker exec` with a glob for the
|
||||
container name which may fail if the compose project name differs. Should use
|
||||
`docker compose exec caddy` instead.
|
||||
|
||||
- The ctrld binary path `/usr/local/bin/ctrld` is the Linux default. On OPNsense
|
||||
(FreeBSD) it installs to a different path. The backend CTRLD_BIN constant
|
||||
should be detected at runtime, not hardcoded.
|
||||
@@ -0,0 +1,413 @@
|
||||
# Avaya / Extreme ERS 5952 Switch Manager
|
||||
|
||||
A browser-based management interface for the Avaya / Extreme Networks ERS 5952 switch. You click buttons. The software figures out the CLI. You never type a switch command.
|
||||
|
||||
Compatible with: ERS 5928, ERS 5948, ERS 5952, ERS 5952-PWR+
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Copy `Avaya_5952_setup.py` to your always-on management computer (Raspberry Pi, HP T620 thin client, old computer or laptop) and run:
|
||||
|
||||
```
|
||||
python Avaya_5952_setup.py
|
||||
```
|
||||
|
||||
That is the entire installation process. The script handles everything automatically. The only step requiring human intervention is loading the SSH public key onto the switch via console cable — the script pauses, shows you exactly what to type, and waits for you to confirm before continuing.
|
||||
|
||||
---
|
||||
|
||||
## What the Setup Script Does
|
||||
|
||||
The script runs 21 steps automatically:
|
||||
|
||||
1. Writes all project files (backend, frontend source, this README)
|
||||
2. Installs system packages (picocom, qrencode, wireguard-tools)
|
||||
3. Adds your user to the dialout group for console cable access
|
||||
4. Installs Python dependencies
|
||||
5. Creates the config directory `/etc/switch-manager/`
|
||||
6. Sets a static IP on your management network interface
|
||||
7. Asks whether to deploy with Docker (recommended) or native Python
|
||||
8. Guides you through console cable setup and software links
|
||||
9. Generates an ed25519 SSH keypair
|
||||
10. Shows you exactly what to paste on the switch console
|
||||
11. Tests the SSH connection and retries if it fails
|
||||
12. Pins the switch host key (MITM protection)
|
||||
13. Generates your TOTP authenticator secret with QR code
|
||||
14. Verifies your authenticator app is working before continuing
|
||||
15. Patches the backend config with your switch IP and credentials
|
||||
16. Builds the React frontend (requires Node.js)
|
||||
17. Installs and enables a systemd service (or Docker container)
|
||||
18. Starts the service
|
||||
19. Optionally sets up Docker with Caddy HTTPS
|
||||
20. Optionally configures WireGuard VPN for remote access
|
||||
21. Optionally installs Control D DNS filtering via ctrld
|
||||
|
||||
Re-running is safe — completed steps are skipped.
|
||||
|
||||
---
|
||||
|
||||
## Deployment Options
|
||||
|
||||
The script explains both options before asking:
|
||||
|
||||
**Docker + Caddy (recommended default)**
|
||||
Runs the switch manager in a container with all dependencies baked in. Caddy provides automatic HTTPS at a hostname you choose (default: `switch.mgmt.lan`). Caddy generates its own internal CA — the setup script installs it into your system trust store automatically so browsers show a clean padlock. Updates with one command: `docker compose pull && docker compose up -d`.
|
||||
|
||||
**Native Python (bare metal)**
|
||||
Runs directly as a Python process managed by systemd. Simpler, lower memory, no Docker required. Access via `http://IP:8765` directly. Right choice for very low memory machines or users who prefer managing services directly.
|
||||
|
||||
---
|
||||
|
||||
## Accessing the Interface
|
||||
|
||||
```
|
||||
http://[management computer IP]:8765
|
||||
```
|
||||
|
||||
Or with Docker + Caddy:
|
||||
```
|
||||
https://switch.mgmt.lan
|
||||
```
|
||||
|
||||
You must be on VLAN 99 (management VLAN) or connected via WireGuard VPN to reach this address. This is intentional — the switch enforces isolation at the network layer.
|
||||
|
||||
### Getting onto VLAN 99 from your laptop
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
sudo ip link add link eth0 name eth0.99 type vlan id 99
|
||||
sudo ip addr add 192.168.99.50/24 dev eth0.99
|
||||
sudo ip link set eth0.99 up
|
||||
```
|
||||
|
||||
**Mac:** System Settings → Network → Add VLAN interface → VLAN ID 99
|
||||
|
||||
**Dedicated port:** Ask the switch to put one port on VLAN 99 as an access port. Plug in when managing.
|
||||
|
||||
**VPN:** Connect via WireGuard (configured during setup or via the VPN tab).
|
||||
|
||||
---
|
||||
|
||||
## The Interface — Eight Tabs
|
||||
|
||||
### Port Map
|
||||
|
||||
Visual 48+4 port chassis, colour-coded by VLAN. Each port shows its VLAN assignment, mode (access/trunk/disabled), and a green dot when PoE is active. Click any port to configure it in the right panel.
|
||||
|
||||
**Port configuration panel:**
|
||||
- Description (e.g. "AP-Corridor-1", "Camera-NE")
|
||||
- Mode: Access, Trunk, or Disabled
|
||||
- VLAN assignment (access) or tagged/native VLANs (trunk)
|
||||
- PoE on/off and wattage limit (copper ports, 1W–30W)
|
||||
|
||||
Live status pulls from the switch every 15 seconds while the tab is active, 60 seconds when backgrounded, and pauses when nobody has the page open.
|
||||
|
||||
### VLANs
|
||||
|
||||
Create, rename, and delete VLANs. Shows port count and subnet per VLAN. VLAN 1 cannot be deleted.
|
||||
|
||||
### ACL Builder
|
||||
|
||||
Build Access Control Lists visually. Each rule specifies action (permit/deny), protocol (ip/tcp/udp/icmp), source, destination, and optional port. The ACL is assigned to a VLAN interface with a direction. The tool generates all CLI syntax — you never write it yourself.
|
||||
|
||||
### Review & Push
|
||||
|
||||
Every change across all tabs is translated into the exact CLI commands the ERS 5952 understands. This tab shows those commands before anything is sent.
|
||||
|
||||
**You never write CLI commands.** The tool generates them. The review step exists so you can inspect what will be sent.
|
||||
|
||||
**Push mode choice:**
|
||||
- **Batch** — all commands sent in sequence, results shown when complete
|
||||
- **Step by step** — one command at a time, confirm each before the next is sent
|
||||
|
||||
Each command is displayed with a plain-English explanation of what it does and what it affects. You can read exactly what is about to happen.
|
||||
|
||||
### Device Access
|
||||
|
||||
Manages which devices can reach the management interface from their normal VLAN without needing VPN. The switch enforces access via an ACL pinhole. TOTP still gates any changes.
|
||||
|
||||
**MAC address randomization warning:** Modern phones and laptops randomize MAC addresses per network. This breaks DHCP reservations. The tab shows per-platform instructions to disable it (iOS, Android, macOS, Windows) before adding a device.
|
||||
|
||||
**Live DHCP leases** pulled from the switch appear as unregistered devices — click Register to add them.
|
||||
|
||||
**Per device:**
|
||||
- Name, MAC, IP, VLAN
|
||||
- Reserve static IP (DHCP binding pushed to switch)
|
||||
- Grant/revoke management access (ACL pinhole pushed to switch)
|
||||
|
||||
### DHCP
|
||||
|
||||
Unified view of all DHCP reservations across switch and OPNsense (if configured).
|
||||
|
||||
**DHCP server recommendation:**
|
||||
- Use switch DHCP for VLAN 99 (management) — devices get IPs before OPNsense is reachable
|
||||
- Use OPNsense for all other VLANs — integrates with DNS, firewall rules, lease history
|
||||
- Never run both for the same VLAN
|
||||
|
||||
**OPNsense integration (optional):**
|
||||
Auto-detects OPNsense at your gateway IP. If found, prompts for API key. Once connected, shows reservations from both switch and OPNsense in one table, colour-coded by source.
|
||||
|
||||
**Conflict detection:** If the same MAC has reservations in both places, a badge appears — red for IP conflicts (same MAC, different IP), yellow for duplicates (same MAC, same IP). Each conflict has a Resolve button with four options: Switch wins, OPNsense wins, Remove from switch, Remove from OPNsense.
|
||||
|
||||
### DNS Filtering
|
||||
|
||||
Configures Control D DNS filtering per VLAN via the `ctrld` daemon.
|
||||
|
||||
**How it works:** `ctrld` runs as a local DNS proxy. Devices send normal DNS queries to it. `ctrld` identifies the source VLAN subnet and routes each query to the correct Control D profile via DoH3. Each VLAN gets different filtering rules. Your ISP sees encrypted HTTPS traffic, not DNS queries.
|
||||
|
||||
**Three deployment options presented with full explanations:**
|
||||
|
||||
*Option A — ctrld on the management computer (fully automated)*
|
||||
Installs ctrld alongside the switch manager. One command downloads and installs it, writes the per-VLAN config, starts it as a system service. The switch DHCP points each VLAN to this machine's IP for DNS.
|
||||
|
||||
*Option B — ctrld on OPNsense (semi-automated)*
|
||||
Generates a single SSH command to paste into OPNsense shell. ctrld installs as a service on OPNsense. OPNsense's IP becomes the DNS server for the network.
|
||||
|
||||
*Option C — manual / existing setup*
|
||||
Generates the `ctrld.toml` config and install command. You install wherever you choose.
|
||||
|
||||
**Per-VLAN Resolver IDs:** Each VLAN gets its own Control D profile. Enter the Resolver ID from the Control D dashboard (controld.com → Add Device → Router → Resolver ID). VLANs without a Resolver ID use the first configured profile as fallback.
|
||||
|
||||
**References:**
|
||||
- Control D documentation: https://docs.controld.com/docs/ctrld
|
||||
- Router setup guide: https://docs.controld.com/docs/routers-platform
|
||||
|
||||
### VPN
|
||||
|
||||
Manages WireGuard VPN for remote access to the management interface from outside the management VLAN.
|
||||
|
||||
**Add clients:** Enter a name (laptop, phone, tablet) and the tool generates a keypair, adds the peer to the server config, reloads WireGuard live, and displays a QR code to scan with the WireGuard app. Also saves a `.conf` file for desktop import.
|
||||
|
||||
**Revoke clients:** Disconnects the peer immediately and removes it from the server config.
|
||||
|
||||
**Connected peers:** Shows last handshake time and transfer stats for each peer.
|
||||
|
||||
**SSH tunnel alternative:** For power users — one command gives secure access without WireGuard installed:
|
||||
```bash
|
||||
ssh -L 8765:localhost:8765 user@management-computer-ip
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Making Changes — Step by Step
|
||||
|
||||
1. **Configure** across any tabs — nothing happens on the switch yet
|
||||
2. **Go to Review & Push** — read the generated commands with explanations
|
||||
3. **Check for dangers** — automatic pre-flight runs before authentication
|
||||
4. **Authenticate** — enter 6-digit TOTP code to unlock a push session
|
||||
5. **Choose push mode** — batch (faster) or step-by-step (full control)
|
||||
6. **Push** — commands go one at a time, checked after each
|
||||
7. **Review results** — each command shows success or failure with the switch's error output
|
||||
8. **Auto-lock** — session closes when push completes. New changes need a new TOTP code
|
||||
|
||||
---
|
||||
|
||||
## Authentication and Sessions
|
||||
|
||||
**Read-only** — no authentication required. Anyone on VLAN 99 can view the dashboard.
|
||||
|
||||
**Push mode** — requires TOTP. One code unlocks exactly one push session. The session closes automatically when the push completes or you cancel. The next set of changes requires a new TOTP code.
|
||||
|
||||
**Why TOTP and not a password:** VLAN 99 isolation is the primary barrier. TOTP adds a second factor confirming it is you making a change. A code is useless after 30 seconds and requires physical access to your authenticator app.
|
||||
|
||||
---
|
||||
|
||||
## How Commands Reach the Switch
|
||||
|
||||
Exact sequence on every push:
|
||||
|
||||
1. Browser sends the command list to the management computer backend
|
||||
2. Backend runs a danger check against known lethal patterns
|
||||
3. Backend validates every command against an allowlist
|
||||
4. Backend opens a fresh SSH connection using the ed25519 key stored on the management computer
|
||||
5. Commands execute one at a time via interactive shell
|
||||
6. After each command the switch's response is checked for error patterns
|
||||
7. On error: push stops immediately, no further commands sent, config not saved
|
||||
8. On full success: `end` then `copy running-config nvram:config.cfg` — config saved to NVRAM
|
||||
|
||||
---
|
||||
|
||||
## Blocked Commands
|
||||
|
||||
**Hard-blocked — refused entirely, must run at the switch console:**
|
||||
|
||||
| Pattern | Reason |
|
||||
|---|---|
|
||||
| `no vlan 99` | Deletes management VLAN |
|
||||
| `vlan members remove ... 99` | Removes VLAN 99 from a port — kills management trunk |
|
||||
| `no vlan tagging ... 99` | Removes VLAN 99 tagging — kills management trunk |
|
||||
| `no ip ssh` | Disables SSH — permanent lockout |
|
||||
| `no ip address` | Removes IP — management computer loses connectivity |
|
||||
| `interface vlan 99` | Modifies management VLAN interface |
|
||||
| `boot config flags factory` | Factory reset |
|
||||
|
||||
**Warning-level — shown for review, push proceeds with confirmation:**
|
||||
|
||||
| Pattern | Reason |
|
||||
|---|---|
|
||||
| `shutdown` | Shuts down an interface — confirm not your uplink |
|
||||
| `default interface` | Resets interface to defaults |
|
||||
| `no vlan [id]` | Deletes a VLAN — confirm no active ports depend on it |
|
||||
| `spanning-tree ... disable` | Disables spanning tree — loop risk |
|
||||
|
||||
---
|
||||
|
||||
## SSH Key Security
|
||||
|
||||
The ed25519 private key lives at `/etc/switch-manager/ers5952_key` on the management computer. It never leaves that machine. Your laptop, phone, or tablet never touches it.
|
||||
|
||||
The switch host key is pinned after the first connection. If the switch's host key ever changes the backend refuses to connect and reports the mismatch — MITM protection even on the management VLAN.
|
||||
|
||||
---
|
||||
|
||||
## Console Cable
|
||||
|
||||
Needed once only — to configure VLAN 99 and load the SSH public key. After that, the ethernet cable from the management computer to the switch handles everything.
|
||||
|
||||
**What to buy:** Search "RJ45 console cable USB Cisco compatible" — get the RJ-45 to USB version (~$8). Works on any laptop with a USB port.
|
||||
|
||||
**Software:**
|
||||
- Windows: PuTTY (putty.org) or TeraTerm
|
||||
- Mac: `screen /dev/tty.usbserial-* 9600` in Terminal
|
||||
- Linux: `sudo picocom -b 9600 /dev/ttyUSB0`
|
||||
|
||||
**Settings:** 9600 baud · 8 data bits · No parity · 1 stop bit · No flow control
|
||||
|
||||
Enter one command at a time. Wait for the `5952(config)#` prompt before sending the next.
|
||||
|
||||
**Switch password rules:** 8–32 characters. Special characters allowed: `! @ # $ % ^ & * - _ = + [ ] ; : , . /`. No spaces. No quotes.
|
||||
|
||||
---
|
||||
|
||||
## Bootstrap — VLAN 99 First-Time Setup
|
||||
|
||||
Before the management tool can run, the switch needs VLAN 99 configured. Connect the console cable and enter these commands one at a time:
|
||||
|
||||
```
|
||||
enable
|
||||
configure terminal
|
||||
vlan create 99 name "Management" type port
|
||||
interface vlan 99
|
||||
ip address 192.168.99.1 255.255.255.0
|
||||
no shutdown
|
||||
exit
|
||||
vlan members add 99 1
|
||||
vlan pvid 1 99
|
||||
ip ssh
|
||||
username admin password YourPassword
|
||||
end
|
||||
copy running-config nvram:config.cfg
|
||||
```
|
||||
|
||||
Replace `1` with the port your management computer plugs into. Replace `YourPassword` with a strong password following the rules above.
|
||||
|
||||
Then give your management computer a static IP:
|
||||
|
||||
```bash
|
||||
# Temporary (immediate, lost on reboot)
|
||||
sudo ip addr add 192.168.99.50/24 dev eth0
|
||||
sudo ip link set eth0 up
|
||||
|
||||
# Find your interface name first: ip link show
|
||||
# Replace eth0 with your actual interface name
|
||||
```
|
||||
|
||||
Verify it works: `ping 192.168.99.1` — then run `python Avaya_5952_setup.py`.
|
||||
|
||||
---
|
||||
|
||||
## Live Polling
|
||||
|
||||
The backend polls the switch using a connection pool:
|
||||
- Pool lifetime: 25 seconds (shorter than switch idle timeout)
|
||||
- Tab active/visible: polls every 15 seconds
|
||||
- Tab backgrounded: polls every 60 seconds
|
||||
- No visitors for 5 minutes: polling pauses completely
|
||||
- New visitor opens page: immediate poll, resumes normal interval
|
||||
|
||||
The switch is never being polled when nobody is looking at the dashboard.
|
||||
|
||||
---
|
||||
|
||||
## Files Created
|
||||
|
||||
| File | Location | Purpose |
|
||||
|---|---|---|
|
||||
| `switch_backend.py` | Project folder | Python API server (34 endpoints) |
|
||||
| `ers5952-manager.jsx` | Project folder | React app source (8 tabs) |
|
||||
| `README.md` | Project folder | This file |
|
||||
| `Dockerfile` | Project folder | Docker image definition |
|
||||
| `docker-compose.yml` | Project folder | Caddy + switch manager services |
|
||||
| `Caddyfile` | Project folder | HTTPS reverse proxy config |
|
||||
| `ctrld.toml` | Project folder | Control D per-VLAN DNS config (if configured) |
|
||||
| `frontend/dist/` | Project folder | Built React app |
|
||||
| `ers5952_key` | `/etc/switch-manager/` | SSH private key (chmod 600) |
|
||||
| `ers5952_key.pub` | `/etc/switch-manager/` | SSH public key |
|
||||
| `known_hosts` | `/etc/switch-manager/` | Pinned switch host key |
|
||||
| `totp_secret` | `/etc/switch-manager/` | TOTP seed (chmod 600) — back this up |
|
||||
| `devices.json` | `/etc/switch-manager/` | Registered device list |
|
||||
| `opnsense.json` | `/etc/switch-manager/` | OPNsense API credentials (chmod 600) |
|
||||
| `ctrld.json` | `/etc/switch-manager/` | Control D config (chmod 600) |
|
||||
| `wg_server_private` | `/etc/switch-manager/` | WireGuard server private key (chmod 600) |
|
||||
| `wg_server_public` | `/etc/switch-manager/` | WireGuard server public key |
|
||||
| `clients/` | `/etc/switch-manager/` | WireGuard client .conf files |
|
||||
| `switch-manager.service` | `/etc/systemd/system/` | Systemd service (native mode) |
|
||||
|
||||
**Back up `/etc/switch-manager/totp_secret`** — if the management computer fails and you have not backed this up you will need to regenerate the TOTP secret and re-scan it into your authenticator app.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Connection banner stuck on "Connecting..."**
|
||||
Backend not running or device not on VLAN 99. Check: `sudo systemctl status switch-manager` or `docker compose ps`
|
||||
|
||||
**"Switch unreachable" in status pill**
|
||||
Backend running but cannot reach switch. Check: `ping 192.168.99.1` from the management computer.
|
||||
|
||||
**TOTP code rejected**
|
||||
Ensure time is synchronised on both the management computer and your phone. Backend allows one 30-second window of clock drift. Sync: `sudo timedatectl set-ntp true`
|
||||
|
||||
**Command failed — switch error shown**
|
||||
Read the error text — the switch says exactly what was wrong. Fix the configuration and push again. Already-succeeded commands do not need to be resent.
|
||||
|
||||
**Host key rejection after switch reset**
|
||||
Re-pin: `ssh-keyscan -H 192.168.99.1 > /etc/switch-manager/known_hosts`
|
||||
|
||||
**ctrld not filtering DNS**
|
||||
Check DHCP option 6 is set to the ctrld machine's IP on each VLAN pool. Check ctrld is running: `ctrld status`. Check the switch is handing out the right DNS: from a device, run `nslookup example.com` and verify the server IP matches.
|
||||
|
||||
**WireGuard not connecting**
|
||||
Check port 51820 UDP is reachable from outside your network (router port forwarding may be needed for external access). Check: `sudo systemctl status wg-quick@wg0`
|
||||
|
||||
**Service logs**
|
||||
```bash
|
||||
# Native
|
||||
journalctl -u switch-manager -f
|
||||
|
||||
# Docker
|
||||
docker compose logs -f
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What This Tool Does Not Do
|
||||
|
||||
- Does not manage OPNsense, pfSense, or any other device directly (OPNsense integration is read/sync only)
|
||||
- Does not provide a terminal or shell — there is no way to type arbitrary commands through the main interface (CLI mode in settings is available for advanced users but still runs through the safety pipeline)
|
||||
- Does not support multiple switches simultaneously
|
||||
- Does not provide traffic analytics or bandwidth graphs
|
||||
- Does not automatically discover or adopt new network devices
|
||||
|
||||
---
|
||||
|
||||
## On the ERS 5952
|
||||
|
||||
This switch has no REST API. Everything this tool does is via SSH sessions that parse text output and send CLI commands — the same thing a human would do at a terminal, automated and wrapped in a browser interface.
|
||||
|
||||
There is an inherent limit to how reliably the tool can detect every possible error condition. The danger blocking system catches the known lethal patterns but cannot anticipate every possible misconfiguration. Use the CLI review step. Read what is about to be sent.
|
||||
|
||||
The console cable is always your fallback. Keep it accessible.
|
||||
@@ -0,0 +1,28 @@
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- switch-manager
|
||||
|
||||
switch-manager:
|
||||
build: .
|
||||
expose:
|
||||
- "8765"
|
||||
volumes:
|
||||
- /etc/switch-manager:/etc/switch-manager:ro
|
||||
- ./frontend/dist:/app/frontend/dist:ro
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
embed_for_dist.py
|
||||
─────────────────
|
||||
Re-embeds edited source files back into Avaya_5952_setup.py
|
||||
for single-file distribution to end users.
|
||||
|
||||
Run after editing source files:
|
||||
python embed_for_dist.py
|
||||
|
||||
This is the reverse of extract_for_dev.py.
|
||||
"""
|
||||
|
||||
import re, sys, ast
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
SETUP = HERE / "Avaya_5952_setup.py"
|
||||
|
||||
if not SETUP.exists():
|
||||
print(f"Error: {SETUP} not found")
|
||||
sys.exit(1)
|
||||
|
||||
src = SETUP.read_text()
|
||||
|
||||
# ── Helper: replace a repr()-encoded constant ───────────────────────────────
|
||||
|
||||
def replace_repr(src, name, new_value):
|
||||
idx = src.find(f'\n{name} = ')
|
||||
if idx == -1:
|
||||
print(f" Warning: {name} not found in setup script")
|
||||
return src
|
||||
start = idx + len(f'\n{name} = ')
|
||||
qc = src[start]
|
||||
end = start + 1
|
||||
while end < len(src):
|
||||
if src[end] == qc and src[end-1] != '\\':
|
||||
end += 1; break
|
||||
end += 1
|
||||
new_repr = repr(new_value)
|
||||
return src[:start] + new_repr + src[end:]
|
||||
|
||||
# ── Helper: replace a triple-quoted constant ────────────────────────────────
|
||||
|
||||
def replace_triple(src, name, new_value):
|
||||
pattern = rf'({name}\s*=\s*""").*?(""")'
|
||||
replacement = r'\g<1>' + new_value.replace('\\', '\\\\') + r'\g<2>'
|
||||
new_src = re.sub(pattern, replacement, src, flags=re.DOTALL)
|
||||
if new_src == src:
|
||||
print(f" Warning: {name} not found or unchanged")
|
||||
return new_src
|
||||
|
||||
# ── Files to embed ───────────────────────────────────────────────────────────
|
||||
|
||||
embeddings = [
|
||||
# (embed_fn, source_filename, constant_name, note)
|
||||
(replace_repr, 'switch_backend.py', 'BACKEND_SRC', "FastAPI backend"),
|
||||
(replace_repr, 'ers5952-manager.jsx', 'JSX_SRC', "React frontend"),
|
||||
(replace_repr, 'README.md', 'README_SRC', "Documentation"),
|
||||
(replace_triple, 'Dockerfile', 'DOCKERFILE', "Docker image"),
|
||||
(replace_triple, 'docker-compose.yml', 'COMPOSE_YML', "Docker Compose"),
|
||||
]
|
||||
|
||||
print("Embedding source files into Avaya_5952_setup.py...\n")
|
||||
|
||||
updated = src
|
||||
changed = []
|
||||
|
||||
for fn, fname, const, note in embeddings:
|
||||
fpath = HERE / fname
|
||||
if not fpath.exists():
|
||||
print(f" ✗ {fname:<35} not found — skipping")
|
||||
continue
|
||||
content = fpath.read_text()
|
||||
new_src = fn(updated, const, content)
|
||||
if new_src != updated:
|
||||
updated = new_src
|
||||
changed.append(fname)
|
||||
print(f" ✓ {fname:<35} {note} ({len(content):,} chars)")
|
||||
else:
|
||||
print(f" – {fname:<35} unchanged")
|
||||
|
||||
if not changed:
|
||||
print("\nNo changes — Avaya_5952_setup.py not modified")
|
||||
sys.exit(0)
|
||||
|
||||
# ── Validate syntax before writing ──────────────────────────────────────────
|
||||
|
||||
print("\nValidating Python syntax...")
|
||||
try:
|
||||
ast.parse(updated)
|
||||
print(" ✓ Syntax OK")
|
||||
except SyntaxError as e:
|
||||
print(f" ✗ Syntax error at line {e.lineno}: {e.msg}")
|
||||
print(" Avaya_5952_setup.py NOT modified — fix the error first")
|
||||
sys.exit(1)
|
||||
|
||||
# ── Write ────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Backup first
|
||||
backup = SETUP.with_suffix('.py.bak')
|
||||
backup.write_text(src)
|
||||
print(f" Backup saved to {backup.name}")
|
||||
|
||||
SETUP.write_text(updated)
|
||||
|
||||
import os
|
||||
size = os.path.getsize(SETUP)
|
||||
print(f"\n✓ Avaya_5952_setup.py updated ({size:,} bytes, {size//1024} KB)")
|
||||
print(f" {len(changed)} file(s) embedded: {', '.join(changed)}")
|
||||
print("""
|
||||
The updated Avaya_5952_setup.py is ready for distribution.
|
||||
Test it: python Avaya_5952_setup.py
|
||||
""")
|
||||
+2600
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
extract_for_dev.py
|
||||
──────────────────
|
||||
Extracts all embedded source files from Avaya_5952_setup.py
|
||||
into individual files for development with Claude Code or any editor.
|
||||
|
||||
Run once to bootstrap the repo:
|
||||
python extract_for_dev.py
|
||||
|
||||
After editing source files, re-embed for distribution:
|
||||
python embed_for_dist.py (see that script)
|
||||
|
||||
The single-file Avaya_5952_setup.py is for end-user distribution.
|
||||
These extracted files are the development source of truth.
|
||||
"""
|
||||
|
||||
import re, sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
SETUP = HERE / "Avaya_5952_setup.py"
|
||||
|
||||
if not SETUP.exists():
|
||||
print(f"Error: {SETUP} not found")
|
||||
sys.exit(1)
|
||||
|
||||
src = SETUP.read_text()
|
||||
|
||||
# ── Extract repr()-encoded string constants ─────────────────────────────────
|
||||
|
||||
def extract_repr(src, name):
|
||||
idx = src.find(f'\n{name} = ')
|
||||
if idx == -1:
|
||||
return None
|
||||
start = idx + len(f'\n{name} = ')
|
||||
qc = src[start]
|
||||
end = start + 1
|
||||
while end < len(src):
|
||||
if src[end] == qc and src[end-1] != '\\':
|
||||
end += 1; break
|
||||
end += 1
|
||||
try:
|
||||
return eval(src[start:end])
|
||||
except Exception as e:
|
||||
print(f" Warning: could not extract {name}: {e}")
|
||||
return None
|
||||
|
||||
# ── Extract triple-quoted string constants ───────────────────────────────────
|
||||
|
||||
def extract_triple(src, name):
|
||||
pattern = rf'{name}\s*=\s*"""(.*?)"""'
|
||||
m = re.search(pattern, src, re.DOTALL)
|
||||
return m.group(1) if m else None
|
||||
|
||||
# ── Files to extract ─────────────────────────────────────────────────────────
|
||||
|
||||
print("Extracting source files from Avaya_5952_setup.py...\n")
|
||||
|
||||
extractions = [
|
||||
# (extract_fn, constant_name, output_filename, note)
|
||||
(extract_repr, 'BACKEND_SRC', 'switch_backend.py', "FastAPI backend"),
|
||||
(extract_repr, 'JSX_SRC', 'ers5952-manager.jsx', "React frontend"),
|
||||
(extract_repr, 'README_SRC', 'README.md', "Documentation"),
|
||||
(extract_triple, 'DOCKERFILE', 'Dockerfile', "Docker image"),
|
||||
(extract_triple, 'COMPOSE_YML', 'docker-compose.yml', "Docker Compose"),
|
||||
(extract_triple, 'CADDYFILE_TEMPLATE','Caddyfile.template', "Caddy config template"),
|
||||
]
|
||||
|
||||
written = []
|
||||
for fn, const, fname, note in extractions:
|
||||
content = fn(src, const)
|
||||
if content:
|
||||
out = HERE / fname
|
||||
if out.exists():
|
||||
overwrite = input(f" {fname} already exists — overwrite? [y/N] ").strip().lower()
|
||||
if overwrite != 'y':
|
||||
print(f" skipped {fname}")
|
||||
continue
|
||||
out.write_text(content)
|
||||
written.append(fname)
|
||||
print(f" ✓ {fname:<35} {note} ({len(content):,} chars)")
|
||||
else:
|
||||
print(f" ✗ {fname:<35} not found in setup script")
|
||||
|
||||
print(f"\n{len(written)} files extracted.")
|
||||
print("""
|
||||
Next steps:
|
||||
1. git init && git add . && git commit -m "initial: extracted from single-file setup"
|
||||
2. Push to GitHub (check no credentials in source first)
|
||||
3. Open with Claude Code: claude
|
||||
4. Say: "Read HANDOFF.md — continue building the switch manager"
|
||||
|
||||
To re-embed source files back into Avaya_5952_setup.py for distribution:
|
||||
python embed_for_dist.py
|
||||
""")
|
||||
+1846
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user