From d33b650af5862014c0604995ccef7bdfdf8ef878 Mon Sep 17 00:00:00 2001 From: jayib Date: Wed, 29 Oct 2025 06:38:25 -0500 Subject: [PATCH] Add LLDP local port ID discovery and mapping via ports_lldplocportid table --- LibreNMS/Modules/Links.php | 45 +++++---- ...60000_create_ports_lldplocportid_table.php | 33 +++++++ includes/discovery/functions.inc.php | 93 +++++++++++++++++++ 3 files changed, 155 insertions(+), 16 deletions(-) create mode 100644 database/migrations/2025-10-15-060000_create_ports_lldplocportid_table.php diff --git a/LibreNMS/Modules/Links.php b/LibreNMS/Modules/Links.php index b72f0046eaf..09213dba3d0 100644 --- a/LibreNMS/Modules/Links.php +++ b/LibreNMS/Modules/Links.php @@ -20,6 +20,10 @@ use LibreNMS\Util\StringHelpers; use SnmpQuery; +/// AVOID the dbFacline() legacy function helpers +use Illuminate\Support\Facades\DB; + + class Links implements Module { use SyncsModels; @@ -151,6 +155,13 @@ private function discoverLldp(Device $device): Collection } if (! empty($lldpRows)) { + + // Create or Update the table ports_lldplocportid with the lldpLocPortId entries for this device, + // along with the LibreNMS device ID and LibreNMS port ID. This makes it possible to look up the + // lldpLocPortId later to make links between device ports that use less standard identifiers (such + // as MAC address) + discoverLldpLocPortId($device); + $oidsremadd = SnmpQuery::hideMib()->numeric()->walk('LLDP-MIB::lldpRemManAddrIfSubtype')->values(); foreach ($oidsremadd as $key => $tmp1) { $res = preg_match("/1\.0\.8802\.1\.1\.2\.1\.4\.2\.1\.3\.([^\.]*)\.([^\.]*)\.([^\.]*)\.([^\.]*)\.([^\.]*).(([^\.]*)(\.([^\.]*))+)/", $key, $matches); @@ -280,31 +291,33 @@ private function discoverLldp(Device $device): Collection $remoteSysName = StringHelpers::linksRemSysName($data['lldpRemSysName']); $remoteSysName = (empty($remoteSysName)) ? StringHelpers::linksRemSysName($data['lldpRemSysDesc']) : $remoteSysName; - $remoteDeviceIp = $data['lldpRemManAddr'] ?? ''; $remoteDeviceId = find_device_id($remoteSysName, $remoteDeviceIp, $remoteMac); - $remotePortId = find_port_id($data['lldpRemPortDesc'] ?? null, $data['lldpRemPortId'], $remoteDeviceId); + // Lookup the device ID and port ID by checking the ports_lldpLocPortId table + $remotePortIdByLldpPortId = find_port_id_by_lldp_port_id($data["lldpRemPortId"], $remoteDeviceId); + + $remotePortId = $remotePortIdByLldpPortId; $remotePortName = (empty($remotePortId)) ? $remotePortName . ' (' . $remoteMac . ')' : $remotePortName; if (! empty($data['localPortId']) && (! empty($remoteSysName))) { - $suffix = (! empty($data['lldpRemManAddr'])) ? '#' . $data['lldpRemManAddr'] : ''; - $links->push(new Link([ - 'local_port_id' => $data['localPortId'], - 'remote_hostname' => $remoteSysName, - 'remote_device_id' => $remoteDeviceId, - 'remote_port_id' => $remotePortId ?? 0, - 'active' => 1, - 'protocol' => 'lldp' . $suffix, - 'remote_port' => $remotePortName, - 'remote_platform' => null, - 'remote_version' => $data['lldpRemSysDesc'] ?? '', - ])); + $suffix = (! empty($data['lldpRemManAddr'])) ? '#' . $data['lldpRemManAddr'] : ''; + $links->push(new Link([ + 'local_port_id' => $data['localPortId'], + 'remote_hostname' => $remoteSysName, + 'remote_device_id' => $remoteDeviceId, + 'remote_port_id' => $remotePortId ?? 0, + 'active' => 1, + 'protocol' => 'lldp' . $suffix, + 'remote_port' => $remotePortName, + 'remote_platform' => null, + 'remote_version' => $data['lldpRemSysDesc'] ?? '', + ])); + } } - } } } - + return $links->filter(); } diff --git a/database/migrations/2025-10-15-060000_create_ports_lldplocportid_table.php b/database/migrations/2025-10-15-060000_create_ports_lldplocportid_table.php new file mode 100644 index 00000000000..b72ae68f265 --- /dev/null +++ b/database/migrations/2025-10-15-060000_create_ports_lldplocportid_table.php @@ -0,0 +1,33 @@ +unsignedInteger('device_id'); + $table->string('lldpLocPortId'); + $table->unsignedInteger('port_id')->nullable(); + $table->unique(['device_id', 'lldpLocPortId']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('ports_lldplocportid'); + } +}; diff --git a/includes/discovery/functions.inc.php b/includes/discovery/functions.inc.php index cecedbe0cc3..6fbf64250c7 100644 --- a/includes/discovery/functions.inc.php +++ b/includes/discovery/functions.inc.php @@ -28,6 +28,70 @@ use LibreNMS\Util\Number; use LibreNMS\Util\UserFuncHelper; +function discoverLldpLocPortId($device) +{ + d_echo('Store lldpLocPortId'); + $module_start = microtime(true); + + $live_ports = []; + $port_stats = []; + $live_ports = snmpwalk_cache_oid($device, 'lldpLocPortId', $port_stats, 'LLDP-MIB'); + + $db_lldp_ports = []; + + // Replaced dbFetchRows() with DB::table to avoid Ignition query-recorder type error when bindings are used + $db_lldp_ports = DB::table('ports_lldplocportid') + ->where('device_id', $device['device_id']) + ->get() + ->map(fn($r) => (array)$r) // convert stdClass to associative array + ->all(); + + $db_ports = []; + $db_ports = DB::table('ports') + ->where('device_id', $device['device_id']) + ->get() + ->map(fn($r) => (array)$r) // convert stdClass → associative array + ->all(); + + foreach ($live_ports as $ifIndex => $snmp_data) { + $db_lldp_index = array_search($snmp_data['lldpLocPortId'], array_column($db_lldp_ports, 'lldpLocPortId')); + $port_index = array_search($ifIndex, array_column($db_ports, 'ifIndex')); + + if (is_int($port_index)) { + $port_id = $db_ports[$port_index]['port_id']; + } else { + $port_id = NULL; + } + $data['device_id'] = $device['device_id']; + $data['lldpLocPortId'] = $snmp_data['lldpLocPortId']; + $data['port_id'] = $port_id; + + if (!is_int($db_lldp_index)) { + d_echo('Adding: ' . $snmp_data['lldpLocPortId']); + dbInsert($data, 'ports_lldplocportid'); + } else { + if ($port_id != $db_lldp_ports[$db_lldp_index]['port_id']) { + d_echo('Updating: ' . $snmp_data['lldpLocPortId'] . ' port_id ' . $db_lldp_ports[$db_lldp_index]['port_id'] . ' to ' . $port_id); + dbUpdate($data, 'ports_lldplocportid', '`device_id` = ? and `lldpLocPortId` = ?', [$data['device_id'], $data['lldpLocPortId']]); + } else { + d_echo('No Change: ' . $snmp_data['lldpLocPortId']); + } + } + } + foreach($db_lldp_ports as $db_data) { + $db_lldp_ports_index = array_search($db_data['lldpLocPortId'], array_column($live_ports, 'lldpLocPortId')); + if (!is_int($db_lldp_ports_index)) { + d_echo('Deleting: ' . $db_data['lldpLocPortId']); + dbDelete('ports_lldplocportid', '`device_id` = ? and `lldpLocPortId` = ?', [$device['device_id'], $db_data['lldpLocPortId']]); + } + } + $module_time = microtime(true) - $module_start; + $module_time = substr($module_time, 0, 5); + d_echo($module_time . ' seconds'); + + //dd('debug stop'); +} + /** * @param string $hostname * @param array $device @@ -911,6 +975,35 @@ function find_device_id($name = '', $ip = '', $mac_address = '') return 0; } + + +/** + * Try to find a port by lldp port id + * + * @param string $lldpremportid matched against ports.lldpLocPortId + * @param int $device_id restrict search to ports on a specific device + * @return int + */ +function find_port_id_by_lldp_port_id($lldpremportid, $device_id) +{ + if (!$device_id) { + return 0; + } + d_echo($lldpremportid . ':' . $device_id); + $sql = "SELECT `port_id` FROM `ports_lldplocportid` WHERE `device_id`=? AND `lldpLocPortId`=? LIMIT 1"; + $params[] = $device_id; + $params[] = $lldpremportid; + + $remote_port_id = (int)dbFetchCell($sql, $params); + if (!$remote_port_id) { + return 0; + } else { + return $remote_port_id; + } +} + + + /** * Try to find a port by ifDescr, ifName, ifAlias, or MAC *