initial commit

This commit is contained in:
Joachim Friberg committed 2026-08-25 14:10:21 +02:00
1 parent af59a90c31
commit ef498e4a72
114 files changed
+11047

No files matched your search

View File
Whitespace-only changes.
+21
View File
@@ -0,0 +1,21 @@
import json
import switch_client
import todo
from extractors.xe_page import fetch_tables
PATH = "/UploadFile.html"
def get_backup_manager(session) -> dict:
"""Returns the current backup-config form fields (method, server
address, etc.) — this page is a config form, not a data table, so the
values reflect whatever was last configured/left blank."""
tables = fetch_tables(session, PATH)
return tables["Backup"]["record"]
if __name__ == "__main__":
session = switch_client.get_session()
print(json.dumps(get_backup_manager(session), indent=2))
todo.mark_implemented(switch_client.BASE_URL + PATH)
+20
View File
@@ -0,0 +1,20 @@
import json
import switch_client
import todo
from extractors.xe_page import fetch_tables
PATH = "/BufferedLogs.html"
def get_buffered_log(session) -> list[dict]:
"""Returns one dict per log entry (Log Index, Severity, Log Time,
Component, Description, Message Text)."""
tables = fetch_tables(session, PATH)
return tables["Log Message"]["rows"]
if __name__ == "__main__":
session = switch_client.get_session()
print(json.dumps(get_buffered_log(session), indent=2))
todo.mark_implemented(switch_client.BASE_URL + PATH)
+20
View File
@@ -0,0 +1,20 @@
import json
import switch_client
import todo
from extractors.xe_page import fetch_tables
PATH = "/Clock_Detail.html"
def get_clock(session) -> dict:
"""Returns {"Current Time": record, "Time Zone": record,
"Daylight Saving Time": record}."""
tables = fetch_tables(session, PATH)
return {caption: table["record"] for caption, table in tables.items()}
if __name__ == "__main__":
session = switch_client.get_session()
print(json.dumps(get_clock(session), indent=2))
todo.mark_implemented(switch_client.BASE_URL + PATH)
+22
View File
@@ -0,0 +1,22 @@
import json
import switch_client
import todo
from extractors.xe_page import fetch_tables
PATH = "/LLDPStats.html"
def get_lldp_statistics(session) -> dict:
"""Returns {"global": record, "interfaces": list[dict]}."""
tables = fetch_tables(session, PATH)
return {
"global": tables["LLDP Global Statistics"]["record"],
"interfaces": tables["LLDP Interface Statistics"]["rows"],
}
if __name__ == "__main__":
session = switch_client.get_session()
print(json.dumps(get_lldp_statistics(session), indent=2))
todo.mark_implemented(switch_client.BASE_URL + PATH)
+18
View File
@@ -0,0 +1,18 @@
import json
import switch_client
import todo
from extractors.xe_page import fetch_tables
PATH = "/Logging.html"
def get_log_configuration(session) -> dict:
tables = fetch_tables(session, PATH)
return tables["Logging"]["record"]
if __name__ == "__main__":
session = switch_client.get_session()
print(json.dumps(get_log_configuration(session), indent=2))
todo.mark_implemented(switch_client.BASE_URL + PATH)
+20
View File
@@ -0,0 +1,20 @@
import json
import switch_client
import todo
from extractors.xe_page import fetch_tables
PATH = "/Network.html"
def get_network_setup(session) -> dict:
"""Returns {table_caption: record} for Network Details, Web Parameters,
Management Access and SNMP."""
tables = fetch_tables(session, PATH)
return {caption: table["record"] for caption, table in tables.items()}
if __name__ == "__main__":
session = switch_client.get_session()
print(json.dumps(get_network_setup(session), indent=2))
todo.mark_implemented(switch_client.BASE_URL + PATH)
+21
View File
@@ -0,0 +1,21 @@
import json
import switch_client
import todo
from extractors.xe_page import fetch_tables
PATH = "/PortSummary.html"
def get_port_summary(session) -> list[dict]:
"""Returns one dict per port (Interface, Physical Type, Port Status,
AutoNeg Status, Link Speed, MTU)."""
tables = fetch_tables(session, PATH)
return tables["Port Summary"]["rows"]
if __name__ == "__main__":
session = switch_client.get_session()
ports = get_port_summary(session)
print(json.dumps(ports, indent=2))
todo.mark_implemented(switch_client.BASE_URL + PATH)
+18
View File
@@ -0,0 +1,18 @@
import json
import switch_client
import todo
from extractors.xe_page import fetch_tables
PATH = "/SNTP.html"
def get_sntp(session) -> dict:
tables = fetch_tables(session, PATH)
return tables["SNTP Configuration"]["record"]
if __name__ == "__main__":
session = switch_client.get_session()
print(json.dumps(get_sntp(session), indent=2))
todo.mark_implemented(switch_client.BASE_URL + PATH)
+18
View File
@@ -0,0 +1,18 @@
import json
import switch_client
import todo
from extractors.xe_page import fetch_tables
PATH = "/SysDescription.html"
def get_system_description(session) -> dict:
tables = fetch_tables(session, PATH)
return tables["System Description"]["record"]
if __name__ == "__main__":
session = switch_client.get_session()
print(json.dumps(get_system_description(session), indent=2))
todo.mark_implemented(switch_client.BASE_URL + PATH)
+76
View File
@@ -0,0 +1,76 @@
"""Generic parser for the switch firmware's auto-generated ("XE") pages.
Every data page (SysDescription.html, PortSummary.html, BufferedLogs.html, ...)
renders one or more <TABLE> elements in one of two shapes:
- "scalar": no <TH> headers, each <TR> is a label/value pair via
<TD class=defleft> (label text) + <TD class=defright><INPUT VALUE=...>.
e.g. Network.html's "Network Details" table.
- "tabular": <TH> column headers followed by one <TR> per data row, each
<TD> holding the cell value inside a hidden <INPUT VALUE=...>.
e.g. PortSummary.html's one row per switch port.
This module parses either shape without needing per-page-specific code; each
extractors/<name>.py module just calls fetch_tables() and picks out the
table(s)/fields it cares about by caption.
"""
from bs4 import BeautifulSoup
import switch_client
def extract_cell_value(td) -> str:
inp = td.find("input")
if inp is not None:
return inp.get("value", "")
return td.get_text(strip=True)
def parse_table(table) -> dict:
caption_tag = table.find("caption")
caption = caption_tag.get_text(strip=True) if caption_tag else None
headers = [th.get_text(strip=True) for th in table.find_all("th", recursive=False)]
trs = table.find_all("tr", recursive=False)
if headers:
rows = []
for tr in trs:
tds = tr.find_all("td", recursive=False)
if len(tds) != len(headers):
continue
rows.append({h: extract_cell_value(td) for h, td in zip(headers, tds)})
return {"caption": caption, "kind": "tabular", "rows": rows}
record = {}
field_names = {}
for tr in trs:
defleft = tr.find("td", class_="defleft")
defright = tr.find("td", class_="defright")
if defleft is None or defright is None:
continue
label = defleft.get_text(strip=True)
if not label:
continue
record[label] = extract_cell_value(defright)
inp = defright.find("input")
if inp is not None and inp.get("name"):
field_names[label] = inp["name"]
# field_names lets a write action look up the real <INPUT NAME=...> for a
# label instead of hardcoding a guessed "v_R_C_1"-style field name.
return {"caption": caption, "kind": "scalar", "record": record, "field_names": field_names}
def parse_xe_tables(html: str) -> list[dict]:
soup = BeautifulSoup(html, "html.parser")
return [parse_table(t) for t in soup.find_all("table")]
def fetch_tables(session, path: str) -> dict[str, dict]:
"""Fetch path and return {caption: table} for every captioned table.
Tables with no/blank caption (usually just a submit button row) are
dropped since there's nothing useful to key them by."""
html = switch_client.fetch(session, path)
tables = parse_xe_tables(html)
return {t["caption"]: t for t in tables if t["caption"]}