Update switch_backend.py for ERS 59100GTS-PWR+ (96+4 port)

Replace ERS 5952 (48+4 port) config with ERS 59100GTS-PWR+:
- Port validation extended to 1–100
- All interfaces now use GigabitEthernet 1/{p} slot notation
- PoE boundary moved from port 48 to port 96
- VLAN commands updated to use 1/{p} port notation
- Key path, TOTP name, and app title updated

https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
This commit is contained in:
Claude
2026-03-24 03:05:44 +00:00
parent 06b87eaaff
commit 12646d4184
+24 -24
View File
@@ -1,5 +1,5 @@
"""
ERS 5952 Switch Manager — Backend v3
ERS 59100GTS-PWR+ Switch Manager — Backend v3
────────────────────────────────────────────────────────────────────────
Changes from v2:
- Connection pool (30s lifetime, liveness check, auto-invalidate)
@@ -40,7 +40,7 @@ log = logging.getLogger("switch-manager")
SWITCH_HOST = "192.168.99.1"
SWITCH_PORT = 22
SWITCH_USER = "admin"
KEY_PATH = "/etc/switch-manager/ers5952_key"
KEY_PATH = "/etc/switch-manager/ers59100_key"
KNOWN_HOSTS = "/etc/switch-manager/known_hosts"
TOTP_FILE = "/etc/switch-manager/totp_secret"
STATIC_DIR = "./frontend/dist"
@@ -71,9 +71,9 @@ def get_or_create_totp_secret() -> str:
def setup_totp():
secret = get_or_create_totp_secret()
uri = pyotp.TOTP(secret).provisioning_uri(
name="ERS5952", issuer_name="SwitchManager")
name="ERS59100", issuer_name="SwitchManager")
print("\n══════════════════════════════════════════════════")
print(" ERS 5952 Switch Manager — TOTP Setup")
print(" ERS 59100GTS-PWR+ Switch Manager — TOTP Setup")
print("══════════════════════════════════════════════════")
print(f"\n Manual entry secret:\n {secret}")
print(f"\n Provisioning URI (paste into authenticator app):\n {uri}")
@@ -217,16 +217,16 @@ def san_vid(v, field="vlan_id") -> int:
return vid
def san_port(v) -> int:
"""Validate and return a port number (152 for the ERS 5952)."""
"""Validate and return a port number (1100 for the ERS 59100GTS-PWR+)."""
p = int(v)
if not 1 <= p <= 52:
raise ValueError("port: must be 152")
if not 1 <= p <= 100:
raise ValueError("port: must be 1100")
return p
_ALLOWED_CMD_RE = [
re.compile(r'^vlan\s+(create|members|tagging|pvid)\s'),
re.compile(r'^no\s+vlan\s+\d+$'),
re.compile(r'^interface\s+(FastEthernet|GigabitEthernet|vlan)\s'),
re.compile(r'^interface\s+(GigabitEthernet\s+1/\d+|vlan\s+\d+)$'),
re.compile(r'^\s+(name|no\s+shutdown|poe|speed|duplex|ip\s+access-group|shutdown)\b'),
re.compile(r'^hostname\s+\S+$'),
re.compile(r'^ip\s+access-list\s+extended\s'),
@@ -600,14 +600,14 @@ class PortConfig(BaseModel):
def build_port(cfg: PortConfig) -> list[str]:
"""
Generate ERS 5952 CLI commands for a port configuration change.
Generate ERS 59100GTS-PWR+ CLI commands for a port configuration change.
Port 148 are FastEthernet; ports 4952 are GigabitEthernet SFP uplinks.
PoE is only available on ports 148.
Ports 196 are GigabitEthernet copper (PoE capable); ports 97100 are SFP+ uplinks (no PoE).
All interfaces use slot/port notation: GigabitEthernet 1/{p}.
Returns a list of CLI command strings ready for push_one_by_one().
"""
p = cfg.port
iface = f"FastEthernet {p}" if p <= 48 else f"GigabitEthernet {p}"
iface = f"GigabitEthernet 1/{p}"
cmds = []
if cfg.description:
cmds += [f"interface {iface}", f' name "{cfg.description}"']
@@ -615,15 +615,15 @@ def build_port(cfg: PortConfig) -> list[str]:
cmds += [f"interface {iface}", " shutdown"]
elif cfg.mode == "access":
vid = san_vid(cfg.access_vlan, "access_vlan")
cmds += [f"vlan members add {vid} {p}", f"vlan pvid {p} {vid}"]
cmds += [f"vlan members add {vid} 1/{p}", f"vlan pvid 1/{p} {vid}"]
elif cfg.mode == "trunk":
native = san_vid(cfg.native_vlan, "native_vlan")
tagged = [san_vid(v, f"tagged_{v}") for v in (cfg.tagged_vlans or [])]
if tagged:
ts = ",".join(str(v) for v in tagged)
cmds += [f"vlan members add {ts} {p}", f"vlan tagging {ts} {p}"]
cmds.append(f"vlan pvid {p} {native}")
if p <= 48:
cmds += [f"vlan members add {ts} 1/{p}", f"vlan tagging {ts} 1/{p}"]
cmds.append(f"vlan pvid 1/{p} {native}")
if p <= 96:
cmds += [f"interface {iface}",
" poe enable" if cfg.poe else " no poe enable"]
if cfg.poe:
@@ -632,7 +632,7 @@ def build_port(cfg: PortConfig) -> list[str]:
def build_acl(acl: AclCreate) -> list[str]:
"""
Generate ERS 5952 CLI commands to create an extended IP ACL and apply it to a VLAN interface.
Generate ERS 59100GTS-PWR+ CLI commands to create an extended IP ACL and apply it to a VLAN interface.
Rules are numbered sequentially starting from 1.
The ACL is applied to the VLAN's Layer 3 interface in the specified direction (in/out).
@@ -657,7 +657,7 @@ def build_acl(acl: AclCreate) -> list[str]:
# APP
# ══════════════════════════════════════════════════════════════════════
app = FastAPI(title="ERS 5952 Switch Manager", version="3.0.0",
app = FastAPI(title="ERS 59100GTS-PWR+ Switch Manager", version="3.0.0",
docs_url="/api/docs", redoc_url=None)
app.add_middleware(CORSMiddleware, allow_origins=ALLOWED_ORIGINS,
@@ -868,7 +868,7 @@ def _save_devices(devices: list):
DEVICES_FILE.write_text(_json.dumps(devices, indent=2))
def _parse_dhcp_leases(raw: str) -> list:
"""Parse ERS 5952 'show dhcp-server leases' output."""
"""Parse ERS 59100GTS-PWR+ 'show dhcp-server leases' output."""
import re
leases = []
for line in raw.splitlines():
@@ -931,7 +931,7 @@ class PinholeRequest(BaseModel):
allow: bool = True
def _build_dhcp_reservation_cmds(device: DeviceEntry) -> list:
"""Generate ERS 5952 CLI for DHCP reservation (static binding)."""
"""Generate ERS 59100GTS-PWR+ CLI for DHCP reservation (static binding)."""
mac_clean = device.mac.replace(':','-').upper()
return [
f"ip dhcp-server static-binding {device.ip}",
@@ -1317,7 +1317,7 @@ def _get_opnsense_leases(cfg: dict) -> list:
return []
def _get_switch_reservations() -> list:
"""Fetch DHCP static bindings from ERS 5952."""
"""Fetch DHCP static bindings from ERS 59100GTS-PWR+."""
import re as _re
try:
raw = read_cmd("show dhcp-server static-binding")
@@ -1369,7 +1369,7 @@ def _get_relay_status() -> dict:
return {"vlans": {}, "ok": False}
def _build_relay_cmds(opnsense_ip: str, vlan_ids: list) -> list:
"""Generate ERS 5952 CLI to set ip helper-address on the specified VLANs."""
"""Generate ERS 59100GTS-PWR+ CLI to set ip helper-address on the specified VLANs."""
cmds = []
for vid in vlan_ids:
cmds += [
@@ -1706,7 +1706,7 @@ def _build_ctrld_toml(vlan_profiles: list, local_domain: str = "lan",
active = [vp for vp in vlan_profiles if vp.get("resolver_id", "").strip()]
lines = [
"# ctrld configuration — generated by Avaya 5952 Switch Manager",
"# ctrld configuration — generated by Avaya 59100GTS-PWR+ Switch Manager",
"# Documentation: https://docs.controld.com/docs/ctrld",
"",
"[service]",
@@ -2212,7 +2212,7 @@ def _generate_dnsmasq_conf(entries: list, mgmt_ip: str = "192.168.99.50") -> str
lines = [
"# dnsmasq — local .lan hostname resolution",
"# Generated by Avaya 5952 Switch Manager",
"# Generated by Avaya 59100GTS-PWR+ Switch Manager",
"# Listens on port 5353 (mapped from Docker container port 53)",
"# ctrld forwards *.lan and *.local here",
"",