feat(lldp): report neighbours from the LLDP Remote Device Summary page
get_lldp_neighbors() was never implemented, so the switch reported no
neighbours and NetOrk's network map had nothing to draw for the site it
serves — on the reference installation two APs, a firewall and a Proxmox host
behind a 1820 8G PoE+ (J9982A) all stood unconnected.
The web UI does carry the data. /htdocs/pages/switching/lldp_remote.lsp is
served with the rows embedded in the page's aDataSet, so the existing _rows()
helper reads it with no new plumbing:
['7', '3', '00:...:44', '00:...:46', 'enp3s0', 'host.example.com',
'bridge, WLAN access point, router, station only', 'bridge', '192.0.2.10']
Column 3 is the neighbour's port ID, which on many peers is a MAC, while
column 4 is its port description — a name a person can read. The description
wins, with the ID used only when it is missing. The chassis ID is passed
through as `mac` when it looks like one, since netOrk resolves a neighbour by
MAC before falling back to its name.
Fixture captured from the live switch and anonymised in the style of the
existing ones.
Closes #1
This commit is contained in:
@@ -30,6 +30,7 @@ from napalm_hpe_officeconnect.client import OfficeConnectClient
|
|||||||
|
|
||||||
logger = logging.getLogger("napalm_hpe_officeconnect")
|
logger = logging.getLogger("napalm_hpe_officeconnect")
|
||||||
|
|
||||||
|
_MAC_RE = re.compile(r"(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")
|
||||||
_SPEED_RE = re.compile(r"(\d+)\s*Mbps", re.IGNORECASE)
|
_SPEED_RE = re.compile(r"(\d+)\s*Mbps", re.IGNORECASE)
|
||||||
|
|
||||||
# Participation values that make an interface a member of a VLAN.
|
# Participation values that make an interface a member of a VLAN.
|
||||||
@@ -76,6 +77,7 @@ class OfficeConnectDriver(SwitchDriver):
|
|||||||
PORT_STATS = "/htdocs/pages/base/port_summary_stats.lsp"
|
PORT_STATS = "/htdocs/pages/base/port_summary_stats.lsp"
|
||||||
MAC_TABLE = "/htdocs/pages/base/mac_address_table.lsp"
|
MAC_TABLE = "/htdocs/pages/base/mac_address_table.lsp"
|
||||||
VLAN_STATUS = "/htdocs/pages/switching/vlan_status.lsp"
|
VLAN_STATUS = "/htdocs/pages/switching/vlan_status.lsp"
|
||||||
|
LLDP_REMOTE = "/htdocs/pages/switching/lldp_remote.lsp"
|
||||||
VLAN_PER_PORT = "/htdocs/pages/switching/vlan_per_port.lsp?vlan={vlan}"
|
VLAN_PER_PORT = "/htdocs/pages/switching/vlan_per_port.lsp?vlan={vlan}"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -222,6 +224,43 @@ class OfficeConnectDriver(SwitchDriver):
|
|||||||
"""Like :meth:`get_vlans` but keeping tagged and untagged apart."""
|
"""Like :meth:`get_vlans` but keeping tagged and untagged apart."""
|
||||||
return self._vlan_participation()
|
return self._vlan_participation()
|
||||||
|
|
||||||
|
def get_lldp_neighbors(self) -> dict[str, list[dict[str, str]]]:
|
||||||
|
"""Return LLDP neighbours keyed by local port.
|
||||||
|
|
||||||
|
Rows of the "LLDP Remote Device Summary" page, verbatim from a
|
||||||
|
1820 8G PoE+ (J9982A)::
|
||||||
|
|
||||||
|
['7', '3', '00:...:44', '00:...:46', 'enp3s0', 'host.example.com',
|
||||||
|
'bridge, WLAN access point, router, station only', 'bridge',
|
||||||
|
'192.0.2.10']
|
||||||
|
|
||||||
|
Column 3 is the neighbour's port *ID*, which on many peers is a MAC;
|
||||||
|
column 4 is its port *description*, which is a name a person can read.
|
||||||
|
The description is preferred and the ID only used when it is absent.
|
||||||
|
|
||||||
|
Without this the switch reports no neighbours at all, and every device
|
||||||
|
behind it — on the reference installation two APs and a firewall —
|
||||||
|
stands unconnected on the network map.
|
||||||
|
"""
|
||||||
|
neighbors: dict[str, list[dict[str, str]]] = {}
|
||||||
|
for row in self._rows(self.LLDP_REMOTE):
|
||||||
|
if len(row) < 6:
|
||||||
|
continue
|
||||||
|
local_port, chassis_id, port_id, port_desc, sysname = (
|
||||||
|
row[0],
|
||||||
|
row[2],
|
||||||
|
row[3],
|
||||||
|
row[4],
|
||||||
|
row[5],
|
||||||
|
)
|
||||||
|
if not local_port:
|
||||||
|
continue
|
||||||
|
entry = {"hostname": sysname, "port": port_desc or port_id}
|
||||||
|
if _MAC_RE.fullmatch(chassis_id or ""):
|
||||||
|
entry["mac"] = chassis_id.lower()
|
||||||
|
neighbors.setdefault(local_port, []).append(entry)
|
||||||
|
return neighbors
|
||||||
|
|
||||||
def get_mac_address_table(self) -> list[dict[str, Any]]:
|
def get_mac_address_table(self) -> list[dict[str, Any]]:
|
||||||
"""Columns: [VLAN ID, MAC Address, Interface, Interface Index, Status]."""
|
"""Columns: [VLAN ID, MAC Address, Interface, Interface Index, Status]."""
|
||||||
table = []
|
table = []
|
||||||
|
|||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||||
|
<!-- Copyright © 2017-2022 Hewlett Packard Enterprise Development LP. -->
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<title>LLDP Remote Device Summary</title>
|
||||||
|
<link rel="shortcut icon" href="/htdocs/login/favicon.ico?">
|
||||||
|
<link type="text/css" media="screen" rel="stylesheet" href="/htdocs/static/bt1707389424/css/jquery-ui-1.12.1.custom.css">
|
||||||
|
<link type="text/css" media="screen" rel="stylesheet" href="/htdocs/static/bt1707389424/jquery/datatables_1_9_4/css/jquery.dataTables_themeroller.css">
|
||||||
|
<link type="text/css" media="screen" rel="stylesheet" href="/htdocs/static/bt1707389424/css/style_datatables_custom.css">
|
||||||
|
<link type="text/css" media="screen" rel="stylesheet" href="/htdocs/static/bt1707389424/css/style_common_pages.css">
|
||||||
|
<link type="text/css" media="screen" rel="stylesheet" href="/htdocs/static/bt1707389424/css/tabs.css">
|
||||||
|
<!--[if ie 7]>
|
||||||
|
<link rel="stylesheet" href="/htdocs/static/bt1707389424/css/style_browser_fixups_ie7.css" type="text/css">
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if gte ie 7]>
|
||||||
|
<link rel="stylesheet" href="/htdocs/static/bt1707389424/css/style_browser_fixups_ie7plus.css" type="text/css">
|
||||||
|
<![endif]-->
|
||||||
|
<script type="text/javascript" src="/htdocs/static/bt1707389424/jquery/jquery_3_5_1/jquery-3.5.1.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/htdocs/static/bt1707389424/jquery/jquery_ui_1_12_1/jquery-ui-1.12.1.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/htdocs/static/bt1707389424/jquery/datatables_1_9_4/jquery.dataTables.min.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="ui-state-default fp_jqtds_page_body fp_corner_bottom">
|
||||||
|
<a name="focus_top" id="focus_top"></a>
|
||||||
|
|
||||||
|
<div id="fp_div_page_title_bar" class="fp_jqtds_h1">
|
||||||
|
<div id="fp_div_page_title_text">LLDP Remote Device Summary</div>
|
||||||
|
<div id="fp_div_title_bar_tooltip">
|
||||||
|
<a class="fp_anchor_tooltip" href="/htdocs/lang/en_us/help/switching/help_lldp_remote.lsp" target="_blank">
|
||||||
|
<img src="/htdocs/static/bt1707389424/images/icon_help_black24-helpText.png" title="Help" alt="Help" border="0">
|
||||||
|
</a>
|
||||||
|
</div><!-- fp_div_title_bar_tooltip -->
|
||||||
|
</div><!-- fp_div_page_title_bar -->
|
||||||
|
|
||||||
|
<div class="fp_page_container">
|
||||||
|
|
||||||
|
<form id="form1" name="form1" method="post" action="/htdocs/pages/switching/lldp_remote.lsp">
|
||||||
|
|
||||||
|
<div class="fp_sub_container">
|
||||||
|
<div id="dynamic_dt1"></div>
|
||||||
|
</div> <!-- fp_sub_container -->
|
||||||
|
|
||||||
|
<div id="page_buttons" class="fp_button_row">
|
||||||
|
<input class="fp_button ui-button ui-widget ui-state-default fp_primary_button" type="submit" id="b_form1_refresh" name="b_form1_refresh" value="Refresh" onclick="jslib_b_pressed_g='refresh';">
|
||||||
|
</div> <!-- fp_button_row -->
|
||||||
|
|
||||||
|
</form> <!-- form1 -->
|
||||||
|
|
||||||
|
</div><!-- fp_page_container -->
|
||||||
|
|
||||||
|
<script type="text/javascript" language="JavaScript">
|
||||||
|
<!--
|
||||||
|
var jslib_msgs = parent.jslib_msgs_tbl;
|
||||||
|
-->
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript" src="/htdocs/static/bt1707389424/js/web-utils-combo.js"></script>
|
||||||
|
<script type="text/javascript" src="/htdocs/static/bt1707389424/jquery/plugins/jquery-plugins-combo.js"></script>
|
||||||
|
<script type="text/javascript" src="/htdocs/static/bt1707389424/jquery/datatables_1_9_4/jquery.dataTables-plugins.js"></script>
|
||||||
|
<script type="text/javascript" src="/htdocs/lang/en_us/culture/globalize.culture.en-US.20120606.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" language="JavaScript">
|
||||||
|
<!--
|
||||||
|
|
||||||
|
var aDataSet = [
|
||||||
|
['7', '3', '00:11:22:33:44:01', '00:11:22:33:44:02', 'enp3s0', 'host-1.example.com', 'bridge, WLAN access point, router, station only', 'bridge', '<a href="http://192.0.2.10" style="text-decoration: underline" target=_blank>192.0.2.10</a>']
|
||||||
|
,
|
||||||
|
['8', '4', '00:11:22:33:44:01', '00:11:22:33:44:01', 'enp1s0', 'host-1.example.com', 'bridge, WLAN access point, router, station only', 'bridge', '<a href="http://192.0.2.10" style="text-decoration: underline" target=_blank>192.0.2.10</a>']
|
||||||
|
];
|
||||||
|
|
||||||
|
var aColumns = [
|
||||||
|
{ "sTitle": "Interface", "sType": "intf-sort", "sWidth": "8%" },
|
||||||
|
{ "sTitle": "Remote ID", "sType": "natural-sort", "sWidth": "8%" },
|
||||||
|
{ "sTitle": "Chassis ID", "sType": "natural-sort", "sWidth": "12%" },
|
||||||
|
{ "sTitle": "Port ID", "sType": "natural-sort", "sWidth": "12%" },
|
||||||
|
{ "sTitle": "Port Description", "sType": "html", "sWidth": "12%" },
|
||||||
|
{ "sTitle": "System Name", "sType": "html", "sWidth": "12%" },
|
||||||
|
{ "sTitle": "Capabilities Supported", "sType": "html", "sWidth": "12%" },
|
||||||
|
{ "sTitle": "Capabilities Enabled", "sType": "html", "sWidth": "12%" },
|
||||||
|
{ "sTitle": "System ID", "sType": "natural-sort" }
|
||||||
|
];
|
||||||
|
|
||||||
|
$(document).ready(function()
|
||||||
|
{
|
||||||
|
// table pagination and sorting
|
||||||
|
$('#dynamic_dt1').html( '<table cellpadding="0" cellspacing="0" border="0" class="display" id="sorttable1"><\/table>' );
|
||||||
|
fp_dt_create("#sorttable1", { aaData: aDataSet, aoColumns: aColumns });
|
||||||
|
});
|
||||||
|
|
||||||
|
-->
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/javascript" language="JavaScript">
|
||||||
|
<!--
|
||||||
|
$(document).ready( function()
|
||||||
|
{
|
||||||
|
var page_is_valid = true;
|
||||||
|
|
||||||
|
// format all culture-specific elements using the current locale
|
||||||
|
culture_format(parent.culture);
|
||||||
|
|
||||||
|
|
||||||
|
var myNav = navigator.userAgent.toLowerCase();
|
||||||
|
var isIe = false;
|
||||||
|
if (myNav.indexOf('msie') != -1)
|
||||||
|
{
|
||||||
|
var myVer = parseInt(myNav.split('msie')[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((isIe == true) && (myVer < 8)) {
|
||||||
|
$('input[type=radio],[type=checkbox]').live('click', function () {
|
||||||
|
this.blur();
|
||||||
|
this.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(':input').bind('keypress', function(e) {
|
||||||
|
var key = (window.event) ? window.event.keyCode : e.which; // IE vs. FF
|
||||||
|
var $this = $(this);
|
||||||
|
if ( ($this.is(':not(:button, :submit, :reset, textarea)')) ) {
|
||||||
|
return (key != 13);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".fp_button").button();
|
||||||
|
$(".fp_button_cancel").button();
|
||||||
|
|
||||||
|
$('form input[type="reset"]').click(function(e) {
|
||||||
|
if( $('#sorttable1').length ){
|
||||||
|
fp_dt_reset_handler("#sorttable1");
|
||||||
|
}
|
||||||
|
return jslib_form_reset(this.form);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(":submit,:button").click(function(e) {
|
||||||
|
// remember which button was clicked last by storing into hidden field
|
||||||
|
var button = $(this).attr("id");
|
||||||
|
var form_id = $(e.target).closest("form").attr("id");
|
||||||
|
if (form_id)
|
||||||
|
{
|
||||||
|
$("#b_"+form_id+"_clicked").val(button);
|
||||||
|
}
|
||||||
|
page_is_valid = jslib_form_validate(document.forms[0]);
|
||||||
|
|
||||||
|
return page_is_valid;
|
||||||
|
});
|
||||||
|
|
||||||
|
// add a hidden field to each form to record which submit button
|
||||||
|
// was clicked so that it gets presented in the POST data
|
||||||
|
$("form").each(function() {
|
||||||
|
var $this = $(this);
|
||||||
|
var bid = "b_" + $this.attr("id") + "_clicked";
|
||||||
|
var b_clicked = '<input type="hidden" id="' + bid + '" name="' + bid + '">';
|
||||||
|
$(b_clicked).appendTo($this);
|
||||||
|
});
|
||||||
|
$(window).resize(function() {
|
||||||
|
if(this.resizeTO) clearTimeout(this.resizeTO);
|
||||||
|
this.resizeTO = setTimeout(function() {
|
||||||
|
$(this).trigger('resizeEnd');
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
$(window).bind('resizeEnd', function() {
|
||||||
|
iframe_auto_resize();
|
||||||
|
});
|
||||||
|
iframe_auto_resize();
|
||||||
|
$('form input[type="submit"]').click(function(e) {
|
||||||
|
var form_id = $(e.target).closest("form").attr("id");
|
||||||
|
var f_s = "b_"+form_id+"_submit";
|
||||||
|
var f_r = "b_"+form_id+"_refresh";
|
||||||
|
var f_click = $("#b_"+form_id+"_clicked").val();
|
||||||
|
if (((f_click == f_s) || (f_click == f_r)) && page_is_valid)
|
||||||
|
{
|
||||||
|
parent.progress_modal_open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parent.progress_modal_handler(parent.ple.PAGE_LOADED);
|
||||||
|
|
||||||
|
focus_init("focus_top");
|
||||||
|
});
|
||||||
|
-->
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ PAGE_FOR_PATH = {
|
|||||||
OfficeConnectDriver.PORT_SUMMARY: "base_port_summary",
|
OfficeConnectDriver.PORT_SUMMARY: "base_port_summary",
|
||||||
OfficeConnectDriver.MAC_TABLE: "base_mac_address_table",
|
OfficeConnectDriver.MAC_TABLE: "base_mac_address_table",
|
||||||
OfficeConnectDriver.VLAN_STATUS: "switching_vlan_status",
|
OfficeConnectDriver.VLAN_STATUS: "switching_vlan_status",
|
||||||
|
OfficeConnectDriver.LLDP_REMOTE: "switching_lldp_remote",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -194,3 +195,31 @@ def test_get_interfaces_counters_is_not_implemented(driver):
|
|||||||
zeros would look like real counters. See README."""
|
zeros would look like real counters. See README."""
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
driver.get_interfaces_counters()
|
driver.get_interfaces_counters()
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# LLDP — the map has nothing to draw for this site without it
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lldp_neighbors_keys_on_local_port(driver):
|
||||||
|
neighbors = driver.get_lldp_neighbors()
|
||||||
|
assert set(neighbors) == {"7", "8"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lldp_neighbors_reports_hostname_and_remote_port(driver):
|
||||||
|
"""The remote port description beats its MAC — `enp3s0` names something."""
|
||||||
|
entry = driver.get_lldp_neighbors()["7"][0]
|
||||||
|
assert entry["hostname"] == "host-1.example.com"
|
||||||
|
assert entry["port"] == "enp3s0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lldp_neighbors_carries_the_chassis_mac(driver):
|
||||||
|
"""netOrk resolves a neighbour by MAC before falling back to its name."""
|
||||||
|
entry = driver.get_lldp_neighbors()["7"][0]
|
||||||
|
assert entry["mac"] == "00:11:22:33:44:01"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_lldp_neighbors_without_neighbours_is_empty(driver):
|
||||||
|
driver._client.fetch.side_effect = lambda path: "<html><body>no table</body></html>"
|
||||||
|
assert driver.get_lldp_neighbors() == {}
|
||||||
|
|||||||
Reference in New Issue
Block a user