Totally reworked navigation between probes/exporters pages

This commit is contained in:
Matteo Biscosi 2025-12-29 11:04:10 +01:00
parent fbecc9fc01
commit e7392db79f
17 changed files with 827 additions and 410 deletions

View file

@ -1,223 +1,444 @@
--
-- (C) 2019-25 - ntop.org
--
-- This module provides helper utilities to manage
-- Flow Exporters, Probes and their Interfaces.
-- It is mainly used by the ntopng Enterprise UI.
--
-- Retrieve ntop directories
dirs = ntop.getDirs()
-- Extend Lua module search path with standard ntop modules
package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path
-- Load common GUI and GET helpers
require "ntop_utils"
require "lua_utils_gui"
require "lua_utils_get"
-- SNMP utilities are only available in the Pro version
local snmp_utils = nil
if ntop.isPro() then
package.path = dirs.installdir .. "/scripts/lua/pro/modules/?.lua;" ..
package.path
snmp_utils = require "snmp_utils"
package.path = dirs.installdir .. "/scripts/lua/pro/modules/?.lua;" .. package.path
snmp_utils = require "snmp_utils"
end
-- Public module table
local exporters_utils = {}
-- ################################################
-- Interface data formatting helper
-- ################################################
local function formatInterfaceData(exporter_ip, new_ports_list, res, uuid_list,
add_role_to_interfaces)
for _, v in pairs(new_ports_list or {}) do
for id, info in pairsByField(v, "bytes.total", rev) do
local role = nil
local interface_name = format_portidx_name(exporter_ip,
tostring(id), true)
local exporter_name = getProbeName(exporter_ip, true, true, false)
if (add_role_to_interfaces) then
role = snmp_utils.get_snmp_interface_role(exporter_ip, id)
end
res[#res + 1] = {
interface_id = id,
interface_name = interface_name,
exporter_ip = exporter_ip,
exporter_name = exporter_name,
exporter_uuid = uuid_list.exporter_uuid,
probe_uuid = uuid_list.probe_uuid,
ifid = uuid_list.ifid, -- Ifid of the exporter
bytes_sent = info["bytes.out_bytes"],
bytes_rcvd = info["bytes.in_bytes"],
total_bytes = info["bytes.total"],
role = role
}
end
end
---
-- Format interface statistics for a given exporter and append them to a result list.
--
-- @param exporter_ip string Exporter IP address
-- @param new_ports_list table List of interface statistics (ports)
-- @param res table Destination table where formatted entries are appended
-- @param uuid_list table Contains probe_uuid, exporter_uuid and ifid
-- @param add_role_to_interfaces boolean Whether to enrich interfaces with SNMP role
--
local function formatInterfaceData(exporter_ip, new_ports_list, res, uuid_list, add_role_to_interfaces)
-- Iterate over all port groups
for _, v in pairs(new_ports_list or {}) do
-- Sort interfaces by total bytes (descending)
for id, info in pairsByField(v, "bytes.total", rev) do
local role = nil
-- Resolve interface name from exporter IP and interface ID
local interface_name = format_portidx_name(exporter_ip, tostring(id), true)
-- Resolve exporter display name
local exporter_name = getProbeName(exporter_ip, true, true, false)
-- Optionally retrieve interface role via SNMP
if (add_role_to_interfaces) then
role = snmp_utils.get_snmp_interface_role(exporter_ip, id)
end
-- Append formatted interface entry
res[#res + 1] = {
interface_id = id,
interface_name = interface_name,
exporter_ip = exporter_ip,
exporter_name = exporter_name,
exporter_uuid = uuid_list.exporter_uuid,
probe_uuid = uuid_list.probe_uuid,
ifid = uuid_list.ifid, -- ntop interface ID
bytes_sent = info["bytes.out_bytes"],
bytes_rcvd = info["bytes.in_bytes"],
total_bytes = info["bytes.total"],
role = role
}
end
end
end
-- ################################################
-- Exporters Interfaces
-- ################################################
-- @brief: this function returns the list of all the Exporters Interfaces
---
-- Retrieve the list of all exporter interfaces across all probes.
--
-- @param add_role_to_interfaces boolean Whether to add SNMP interface roles
-- @return table List of exporter interfaces
--
function exporters_utils.getAllInterfacesList(add_role_to_interfaces)
local list = {}
local list = {}
local ifstats = interface.getStats()
-- Get the list of all the probes
for ifid, probe_list in pairs(ifstats.probes or {}) do
for _, probe_info in pairsByKeys(probe_list or {}) do
local uuid = probe_info["probe.source_id"]
local probe_ip = probe_info["probe.ip"]
-- Global interface statistics
local ifstats = interface.getStats()
-- For each probe retrieve the list of interfaces
if (uuid) then
if (table.len(probe_info.exporters) == 0) then
-- Packet probe
local ports_table = interface.getFlowDeviceInfo(uuid, true)
local exporter_ip = probe_info["remote.if_addr"]
formatInterfaceData(exporter_ip, ports_table, list, {
probe_uuid = uuid,
exporter_uuid = uuid,
ifid = ifid
}, add_role_to_interfaces)
else
-- Collector probe
local collector_value = 0
-- Iterate over all probes grouped by interface ID
for ifid, probe_list in pairs(ifstats.probes or {}) do
for _, probe_info in pairsByKeys(probe_list or {}) do
local uuid = probe_info["probe.source_id"]
local probe_ip = probe_info["probe.ip"]
for exporter_ip, exporter_info in pairsByKeys(
probe_info.exporters or
{}) do
local ports_table =
interface.getFlowDeviceInfo(
exporter_info.unique_source_id, true)
-- Ensure probe has a valid UUID
if (uuid) then
if (table.len(probe_info.exporters) == 0) then
-- Packet probe (no exporters, traffic captured locally)
local ports_table = interface.getFlowDeviceInfo(uuid, true)
local exporter_ip = probe_info["remote.if_addr"]
formatInterfaceData(exporter_ip, ports_table, list, {
probe_uuid = uuid,
exporter_uuid = unique_source_id,
ifid = ifid
}, add_role_to_interfaces)
end
end
end
end
end
return list
end
-- ################################################
-- @brief: this function returns the list of all the Exporters Interfaces
function exporters_utils.getAllProbesList()
local ifnames = interface.getIfNames()
local ifstats = interface.getStats()
local list = {}
-- Get the list of all the probes
for ifid, probe_list in pairs(ifstats.probes or {}) do
for _, probe_info in pairsByKeys(probe_list or {}) do
local uuid = probe_info["probe.source_id"]
local probe_ip = probe_info["probe.ip"]
if probe_info.exporters and table.len(probe_info.exporters) > 0 then -- Sflow or NetFlow/IPFIX
for exporter_ip, exporter_info in pairsByKeys(
probe_info.exporters or {},
asc) do
local name = getProbeName(exporter_ip, true, false, false)
local num_flows = exporter_info.num_netflow_flows or
exporter_info.num_sflow_flows or 0
if ifstats.isView then
name =
name .. " [ " .. ifnames[tostring(ifid)] ..
"]"
end
list[#list + 1] = {
name = name,
ip = exporter_ip,
unique_source_id = exporter_info.unique_source_id,
ifid = ifid
}
end
formatInterfaceData(exporter_ip, ports_table, list, {
probe_uuid = uuid,
exporter_uuid = uuid,
ifid = ifid
}, add_role_to_interfaces)
else
local name = getProbeName(probe_ip, true, false, false)
if ifstats.isView then
name = name .. " [ " .. ifnames[tostring(ifid)] ..
"]"
end
list[#list + 1] = {
name = name,
unique_source_id = probe_info["probe.source_id"],
ip = probe_ip,
ifid = ifid
}
end
end
end
-- Collector probe (NetFlow / IPFIX / sFlow)
for exporter_ip, exporter_info in pairsByKeys(probe_info.exporters or {}) do
local ports_table = interface.getFlowDeviceInfo(exporter_info.unique_source_id, true)
return list
formatInterfaceData(exporter_ip, ports_table, list, {
probe_uuid = uuid,
exporter_uuid = unique_source_id,
ifid = ifid
}, add_role_to_interfaces)
end
end
end
end
end
return list
end
-- ################################################
-- Probes List
-- ################################################
---
-- Retrieve the list of all probes and exporters.
--
-- @return table List of probes/exporters metadata
--
function exporters_utils.getAllProbesList()
local ifnames = interface.getIfNames()
local ifstats = interface.getStats()
local list = {}
-- Iterate over all probes
for ifid, probe_list in pairs(ifstats.probes or {}) do
for _, probe_info in pairsByKeys(probe_list or {}) do
local uuid = probe_info["probe.source_id"]
local probe_ip = probe_info["probe.ip"]
-- Flow-based probes (NetFlow / sFlow)
if probe_info.exporters and table.len(probe_info.exporters) > 0 then
for exporter_ip, exporter_info in pairsByKeys(probe_info.exporters or {}, asc) do
local name = getProbeName(exporter_ip, true, false, false)
if ifstats.isView then
name = name .. " [ " .. ifnames[tostring(ifid)] .. "]"
end
list[#list + 1] = {
name = name,
ip = exporter_ip,
unique_source_id = exporter_info.unique_source_id,
ifid = ifid
}
end
else
-- Packet probe
local name = getProbeName(probe_ip, true, false, false)
if ifstats.isView then
name = name .. " [ " .. ifnames[tostring(ifid)] .. "]"
end
list[#list + 1] = {
name = name,
unique_source_id = uuid,
ip = probe_ip,
ifid = ifid
}
end
end
end
return list
end
-- ################################################
-- Exporter UUID resolution (with cache)
-- ################################################
-- Cache: exporter_ip -> { exporter_uuid, ifid }
local _exporter_uuid = {}
---
-- Retrieve exporter UUID and interface ID from exporter IP.
--
-- @param exporter_ip string
-- @return string exporter_uuid
-- @return number ifid
--
function exporters_utils.getExporterUUID(exporter_ip)
local ret = _exporter_uuid[exporter_ip]
local ret = _exporter_uuid[exporter_ip]
if (ret ~= nil) then
return ret
end
if (ret ~= nil) then return ret end
if not isEmptyString(exporter_ip) then
local flow_exporters = interface.getFlowDevices()
if not isEmptyString(exporter_ip) then
local flow_exporters = interface.getFlowDevices()
for ifid, info in pairs(flow_exporters or {}) do
for exporter_uuid, exporter_info in pairs(info or {}) do
if exporter_info.exporter_ip == exporter_ip then
_exporter_uuid[exporter_ip] = {exporter_uuid, ifid}
return exporter_uuid, ifid
end
for ifid, info in pairs(flow_exporters or {}) do
for exporter_uuid, exporter_info in pairs(info or {}) do
if exporter_info.exporter_ip == exporter_ip then
_exporter_uuid[exporter_ip] = {exporter_uuid, ifid}
return exporter_uuid, ifid
end
end
end
end
end
end
return nil, nil
return nil, nil
end
-- ################################################
-- Probe UUID resolution (with cache)
-- ################################################
-- Cache: exporter_ip -> { probe_uuid, ifid }
local _probe_uuid = {}
---
-- Retrieve probe UUID associated with a given exporter IP.
--
-- @param exporter_ip string
-- @return string probe_uuid
-- @return number ifid
--
function exporters_utils.getProbeUUID(exporter_ip)
local ret = _probe_uuid[exporter_ip]
local ret = _probe_uuid[exporter_ip]
if (ret ~= nil) then
return ret
end
if (ret ~= nil) then return ret end
if not isEmptyString(exporter_ip) then
local exporter_uuid = nil
local flow_exporters = interface.getFlowDevices()
if not isEmptyString(exporter_ip) then
local exporter_uuid = nil
local flow_exporters = interface.getFlowDevices()
for ifid, info in pairs(flow_exporters or {}) do
for uuid, exporter_info in pairs(info or {}) do
if exporter_info.exporter_ip == exporter_ip then
exporter_uuid = uuid
goto uuid_found
end
-- Resolve exporter UUID
for ifid, info in pairs(flow_exporters or {}) do
for uuid, exporter_info in pairs(info or {}) do
if exporter_info.exporter_ip == exporter_ip then
exporter_uuid = uuid
goto uuid_found
end
end
::uuid_found::
if (exporter_uuid) then
local ifstats = interface.getStats()
-- Get the list of all the probes
for ifid, probe_list in pairs(ifstats.probes or {}) do
for probe_uuid, probe_info in pairsByKeys(probe_list or {}) do
if tostring(probe_uuid) == tostring(exporter_uuid) then
-- Packet interface
_probe_uuid[exporter_ip] = {probe_uuid, ifid}
return probe_uuid, ifid
end
for _, exporter_info in pairs(probe_info.exporters or {}) do
if tostring(exporter_info.unique_source_id) ==
tostring(exporter_uuid) then
-- Netflow Interface
_probe_uuid[exporter_ip] = {probe_uuid, ifid}
return probe_uuid, ifid
end
end
end
end
end
end
end
end
::uuid_found::
return nil, nil
-- Map exporter UUID to probe UUID
if (exporter_uuid) then
local ifstats = interface.getStats()
for ifid, probe_list in pairs(ifstats.probes or {}) do
for probe_uuid, probe_info in pairsByKeys(probe_list or {}) do
if tostring(probe_uuid) == tostring(exporter_uuid) then
-- Packet probe
_probe_uuid[exporter_ip] = {probe_uuid, ifid}
return probe_uuid, ifid
end
for _, exporter_info in pairs(probe_info.exporters or {}) do
if tostring(exporter_info.unique_source_id) == tostring(exporter_uuid) then
-- Flow exporter
_probe_uuid[exporter_ip] = {probe_uuid, ifid}
return probe_uuid, ifid
end
end
end
end
end
end
return nil, nil
end
-- ################################################
-- Navbar helpers
-- ################################################
---
-- Build the navigation bar title with breadcrumbs.
--
-- @param ip string Exporter IP
-- @param nprobe_info table Probe metadata
-- @return string HTML title
--
local function build_navbar_title(ip, nprobe_info)
local navbar_title = i18n("flow_devices.nprobe_instances")
if nprobe_info then
local breadcrumb = "<span>"
local probe_ip = nprobe_info["probe.ip"]
local probe_name = getProbeName(probe_ip, true, true, false)
breadcrumb = breadcrumb .. " | " .. probe_name .. " (" .. i18n("flow_devices.probe") .. ")"
if not isEmptyString(ip) and ip ~= probe_ip then
local exporter_name = getProbeName(ip, true, true, false)
breadcrumb = breadcrumb .. " / " .. exporter_name .. " (" .. i18n("flow_devices.exporter") .. ") "
end
breadcrumb = breadcrumb .. "</span>"
navbar_title = navbar_title .. breadcrumb
end
return navbar_title
end
-- ################################################
-- Navbar rendering
-- ################################################
---
-- Print the exporters navigation bar.
--
-- @param ifid number Interface ID
-- @param page string Active page
-- @param ip string Exporter IP
-- @param probe_uuid string Probe UUID
--
function exporters_utils.printNavbar(ifid, page, ip, probe_uuid)
local page_utils = require("page_utils")
-- URLs and state flags initialization
local interfaces_url = ntop.getHttpPrefix() .. "/lua/pro/enterprise/exporter_interfaces.lua"
local exporter_url = ntop.getHttpPrefix() .. "/lua/pro/enterprise/exporters.lua"
local snmp_available = false
local nprobe_info = nil
local conf_url = ""
local timeseries_url = ""
local snmp_url = ""
-- Resolve probe information if available
if not isEmptyString(probe_uuid) then
nprobe_info = getProbeFromUUID(probe_uuid)
if isEmptyString(ip) and nprobe_info then
ip = nprobe_info["probe.ip"]
end
end
local title_navbar = build_navbar_title(ip, nprobe_info)
-- Check SNMP availability
snmp_available = exporters_utils.isSNMPAvailable(ip)
-- Append parameters
if not isEmptyString(probe_uuid) then
interfaces_url = interfaces_url .. "?probe_uuid=" .. probe_uuid
exporter_url = exporter_url .. "?probe_uuid=" .. probe_uuid
if not isEmptyString(ip) then
local _, tmp1 = exporters_utils.getProbeUUID(ip)
conf_url = ntop.getHttpPrefix() .. "/lua/pro/enterprise/exporter_interfaces.lua?ip=" .. ip .. "&ifid=" .. tmp1 ..
"&page=config&probe_uuid=" .. probe_uuid
timeseries_url = ntop.getHttpPrefix() .. "/lua/pro/enterprise/exporter_details.lua?ip=" .. ip .. "&ifid=" .. tmp1 ..
"&page=historical&probe_uuid=" .. probe_uuid
snmp_url = ntop.getHttpPrefix() .. "/lua/pro/enterprise/snmp_device_details.lua?host=" .. ip
end
end
-- Render navbar
page_utils.print_navbar(title_navbar, ntop.getHttpPrefix() .. "/lua/pro/enterprise/nprobe.lua", {{
active = page == "nprobe",
page_name = "overview",
label = "<i class=\"fas fa-lg fa-home\" data-bs-toggle=\"tooltip\" " .. "title=\"" .. i18n("flow_devices.exporters_menu_entry") ..
"\"></i>"
}, {
url = exporter_url,
page_name = "exporters",
active = (page == "exporters"),
label = i18n("flow_devices.flow_exporters")
}, {
url = interfaces_url,
page_name = "interfaces",
active = page == "interfaces",
label = i18n("flow_devices.exporters_interfaces")
}, {
hidden = not snmp_available or isEmptyString(ip),
url = snmp_url,
page_name = "snmp",
label = i18n("if_stats_overview.snmp")
}, {
active = page == "historical",
page_name = "historical",
hidden = isEmptyString(ip),
url = timeseries_url,
label = "<i class=\"fas fa-lg fa-chart-area\" data-bs-toggle=\"tooltip\" " .. "title=\"" .. i18n("prefs.timeseries") .. "\"></i>"
}, {
active = page == "config",
page_name = "config",
hidden = isEmptyString(ip),
url = conf_url,
label = "<i class=\"fas fa-lg fa-cog\" data-bs-toggle=\"tooltip\" " .. "title=\"" .. i18n("flow_checks.callback_config") .. "\"></i>"
}})
end
-- ################################################
--
-- Check whether SNMP information is available for a given device.
--
-- This function verifies if SNMP system information for the specified
-- device IP is present in the SNMP cache. If system data exists, SNMP
-- is considered available for that device.
--
-- @brief Check SNMP availability for a device
-- @param device_ip string The IP address of the device to check
-- @return boolean true if SNMP data is available, false otherwise
--
function exporters_utils.isSNMPAvailable(device_ip)
-- Ensure the device IP is valid and not empty
if not isEmptyString(device_ip) then
local snmp_cached_dev = require "snmp_cached_dev"
-- Retrieve cached SNMP system information for the device
-- NOTE: cached_system_info is expected to always be a table
local cached_system_info = snmp_cached_dev:get_system(device_ip) or {}
-- cached_system_info.system contains SNMP system data
-- If it has at least one entry, SNMP is considered available
-- The comment below assumes that cached_system_info is never nil
if table.len(cached_system_info.system) > 0 then
return true
end
end
-- SNMP data not available or invalid device IP
return false
end
-- ################################################
-- Return public module
return exporters_utils