feat: NAPALM driver for HPE OfficeConnect 1820/1920S
These switches have no CLI at all — no SSH, no Telnet and no ArubaOS-Switch REST API — so the ProCurve driver cannot serve them despite the shared vendor. The only management surface is the web UI, which ships its table data as JavaScript array literals; those parse with ast.literal_eval, so the driver needs no HTML parser and no dependency beyond napalm/requests. Read-only by design: the platform exposes a single administrator account with no privilege levels, and serves HTTPS only after a certificate has been uploaded, so the polling credential is necessarily the admin credential over a plain channel. Implements get_facts, get_interfaces, get_vlans, get_vlans_detail and get_mac_address_table, plus HTTP/SNMP fingerprints for discovery. Tested against an HPE OfficeConnect 1820 8G PoE+ (65W), J9982A, PT.02.19.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"""Unit tests for the OfficeConnect HTML/JS parsers.
|
||||
|
||||
Fixtures are real pages captured from an HPE OfficeConnect 1820 8G PoE+
|
||||
(J9982A, firmware PT.02.19), scrubbed of serial number, MAC addresses,
|
||||
IP addresses and hostname.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
from napalm_hpe_officeconnect import parsers
|
||||
|
||||
FIXTURES = pathlib.Path(__file__).parent.parent / "fixtures"
|
||||
|
||||
|
||||
def fixture(name: str) -> str:
|
||||
return (FIXTURES / f"{name}.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# extract_data_set — the DataTables payload every list page ships
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_extract_data_set_returns_one_row_per_port():
|
||||
"""port_summary carries 8 physical ports plus 4 trunk interfaces."""
|
||||
rows = parsers.extract_data_set(fixture("base_port_summary"))
|
||||
assert len(rows) == 12
|
||||
assert all(isinstance(r, list) for r in rows)
|
||||
|
||||
|
||||
def test_extract_data_set_preserves_cell_values():
|
||||
rows = parsers.extract_data_set(fixture("base_port_summary"))
|
||||
# Columns: [checkbox, interface, description, admin, phy type, status, ...]
|
||||
assert rows[0][1] == "1"
|
||||
assert rows[0][3] == "Enabled"
|
||||
assert rows[2][5] == "Link Up"
|
||||
assert rows[2][7] == "1000 Mbps"
|
||||
|
||||
|
||||
def test_extract_data_set_handles_rows_without_markup():
|
||||
"""mac_address_table rows are plain values, no embedded checkbox cell."""
|
||||
rows = parsers.extract_data_set(fixture("base_mac_address_table"))
|
||||
assert len(rows) == 17
|
||||
assert rows[0] == ["1", "00:11:22:33:44:01", "7", "7", "Learned"]
|
||||
|
||||
|
||||
def test_extract_data_set_handles_empty_cells():
|
||||
"""Counter columns are empty strings on a port that never linked up."""
|
||||
rows = parsers.extract_data_set(fixture("base_port_summary_stats"))
|
||||
assert rows[0] == ["1", "", "", "", "", "", "", "", ""]
|
||||
|
||||
|
||||
def test_extract_data_set_returns_empty_list_when_absent():
|
||||
assert parsers.extract_data_set("<html><body>nothing here</body></html>") == []
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# extract_column_titles — used to assert the firmware layout hasn't shifted
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_extract_column_titles():
|
||||
titles = parsers.extract_column_titles(fixture("switching_vlan_status"))
|
||||
assert titles == ["VLAN ID", "Name", "Type"]
|
||||
|
||||
|
||||
def test_extract_column_titles_skips_markup_only_header():
|
||||
"""The leading checkbox column has an <input> as its title, not text."""
|
||||
titles = parsers.extract_column_titles(fixture("base_mac_address_table"))
|
||||
assert titles == ["VLAN ID", "MAC Address", "Interface", "Interface Index", "Status"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# strip_markup — the checkbox cell must collapse to an empty string
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw,expected",
|
||||
[
|
||||
('<input type="checkbox" value="1"><label></label>', ""),
|
||||
("plain value", "plain value"),
|
||||
(" spaced&out ", "spaced&out"),
|
||||
("", ""),
|
||||
],
|
||||
)
|
||||
def test_strip_markup(raw, expected):
|
||||
assert parsers.strip_markup(raw) == expected
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# parse_uptime
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw,expected",
|
||||
[
|
||||
("0 days, 18 hours, 44 mins, 40 secs", 67480),
|
||||
("3 days, 0 hours, 0 mins, 0 secs", 259200),
|
||||
("0 days, 0 hours, 0 mins, 1 secs", 1),
|
||||
],
|
||||
)
|
||||
def test_parse_uptime(raw, expected):
|
||||
assert parsers.parse_uptime(raw) == expected
|
||||
|
||||
|
||||
def test_parse_uptime_returns_negative_one_when_unparseable():
|
||||
"""NAPALM's convention for 'uptime unknown' is -1, not 0."""
|
||||
assert parsers.parse_uptime("nonsense") == -1
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# parse_facts — dashboard.lsp
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_parse_facts():
|
||||
facts = parsers.parse_facts(fixture("base_dashboard"))
|
||||
assert facts["hostname"] == "testswitch"
|
||||
assert facts["serial_number"] == "CN0FIXTURE0"
|
||||
assert facts["os_version"] == "PT.02.19"
|
||||
assert facts["model"] == "HPE OfficeConnect Switch 1820 8G PoE+ (65W) J9982A"
|
||||
assert facts["uptime"] == 67480
|
||||
assert facts["sys_object_id"] == "1.3.6.1.4.1.11.2.3.7.11.170"
|
||||
|
||||
|
||||
def test_parse_facts_tolerates_missing_fields():
|
||||
facts = parsers.parse_facts("<html><body></body></html>")
|
||||
assert facts["hostname"] == ""
|
||||
assert facts["serial_number"] == ""
|
||||
assert facts["uptime"] == -1
|
||||
Reference in New Issue
Block a user