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