mirror of
https://github.com/ntop/ntopng.git
synced 2026-04-29 23:49:33 +00:00
The score is now calculated differently on the client and on the server of the flow. The hosts flow is updated every minute and charted. It's now possible to trigger an alert when the score threshold is exceeded
1457 lines
45 KiB
Lua
1457 lines
45 KiB
Lua
--
|
|
-- (C) 2013-20 - ntop.org
|
|
--
|
|
require "lua_utils"
|
|
require "db_utils"
|
|
require "historical_utils"
|
|
require "rrd_paths"
|
|
local dkjson = require("dkjson")
|
|
local host_pools_utils = require "host_pools_utils"
|
|
local top_talkers_utils = require "top_talkers_utils"
|
|
local os_utils = require "os_utils"
|
|
local have_nedge = ntop.isnEdge()
|
|
|
|
local ts_utils = require("ts_utils")
|
|
local ts_common = require("ts_common")
|
|
|
|
-- ########################################################
|
|
|
|
if(ntop.isPro()) then
|
|
package.path = dirs.installdir .. "/pro/scripts/lua/modules/?.lua;" .. package.path
|
|
require "nv_graph_utils"
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
local graph_colors = {
|
|
'#1f77b4',
|
|
'#ff7f0e',
|
|
'#2ca02c',
|
|
'#d62728',
|
|
'#9467bd',
|
|
'#8c564b',
|
|
'#e377c2',
|
|
'#7f7f7f',
|
|
'#bcbd22',
|
|
'#17becf',
|
|
-- https://github.com/mbostock/d3/wiki/Ordinal-Scales
|
|
'#ff7f0e',
|
|
'#ffbb78',
|
|
'#1f77b4',
|
|
'#aec7e8',
|
|
'#2ca02c',
|
|
'#98df8a',
|
|
'#d62728',
|
|
'#ff9896',
|
|
'#9467bd',
|
|
'#c5b0d5',
|
|
'#8c564b',
|
|
'#c49c94',
|
|
'#e377c2',
|
|
'#f7b6d2',
|
|
'#7f7f7f',
|
|
'#c7c7c7',
|
|
'#bcbd22',
|
|
'#dbdb8d',
|
|
'#17becf',
|
|
'#9edae5'
|
|
}
|
|
|
|
-- ########################################################
|
|
|
|
-- @brief Ensure that the provided series have the same number of points. This is a
|
|
-- requirement for the charts.
|
|
-- @param series a list of series to fix. The format of each serie is the one
|
|
-- returned by ts_utils.query
|
|
-- @note the series are modified in place
|
|
function normalizeSeriesPoints(series)
|
|
local max_count = 0
|
|
local min_step = math.huge
|
|
|
|
for _, serie in pairs(series) do
|
|
max_count = math.max(max_count, #serie.series[1].data)
|
|
min_step = math.min(min_step, serie.step)
|
|
end
|
|
|
|
if max_count > 0 then
|
|
for _, serie in pairs(series) do
|
|
local count = #serie.series[1].data
|
|
|
|
if count ~= max_count then
|
|
for _, serie in pairs(serie.series) do
|
|
serie.data = ts_common.upsampleSerie(serie.data, max_count)
|
|
serie.step = min_step
|
|
serie.count = max_count
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
function queryEpochData(schema, tags, selectedEpoch, zoomLevel, options)
|
|
if(zoomLevel == nil) then zoomLevel = "1h" end
|
|
local d = getZoomDuration(zoomLevel)
|
|
local end_time
|
|
local start_time
|
|
options = table.merge(options or {}, {initial_point=true})
|
|
|
|
if((selectedEpoch == nil) or (selectedEpoch == "")) then
|
|
selectedEpoch = os.time()
|
|
end_time = tonumber(selectedEpoch)
|
|
start_time = end_time - d
|
|
else
|
|
end_time = tonumber(selectedEpoch) + math.floor(d / 2)
|
|
start_time = tonumber(selectedEpoch) - math.floor(d / 2)
|
|
end
|
|
|
|
return ts_utils.query(schema, tags, start_time, end_time, options)
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
function getProtoVolume(ifName, start_time, end_time)
|
|
ifId = getInterfaceId(ifName)
|
|
local series = ts_utils.listSeries("iface:ndpi", {ifid=ifId}, start_time)
|
|
|
|
ret = { }
|
|
for _, tags in ipairs(series or {}) do
|
|
-- NOTE: this could be optimized via a dedicated driver call
|
|
local data = ts_utils.query("iface:ndpi", tags, start_time, end_time)
|
|
|
|
if(data ~= nil) and (data.statistics.total > 0) then
|
|
ret[tags.protocol] = data.statistics.total
|
|
end
|
|
end
|
|
|
|
return(ret)
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
function breakdownBar(sent, sentLabel, rcvd, rcvdLabel, thresholdLow, thresholdHigh)
|
|
if((sent+rcvd) > 0) then
|
|
sent2rcvd = round((sent * 100) / (sent+rcvd), 0)
|
|
-- io.write("****>> "..sent.."/"..rcvd.."/"..sent2rcvd.."\n")
|
|
if((thresholdLow == nil) or (thresholdLow < 0)) then thresholdLow = 0 end
|
|
if((thresholdHigh == nil) or (thresholdHigh > 100)) then thresholdHigh = 100 end
|
|
|
|
if(sent2rcvd < thresholdLow) then sentLabel = '<i class="fas fa-exclamation-triangle fa-lg"></i> '..sentLabel
|
|
elseif(sent2rcvd > thresholdHigh) then rcvdLabel = '<i class="fas fa-exclamation-triangle fa-lg""></i> '..rcvdLabel end
|
|
|
|
print('<div class="progress"><div class="progress-bar bg-warning" aria-valuenow="'.. sent2rcvd..'" aria-valuemin="0" aria-valuemax="100" style="width: ' .. sent2rcvd.. '%;">'..sentLabel)
|
|
print('</div><div class="progress-bar bg-info" aria-valuenow="'.. (100-sent2rcvd)..'" aria-valuemin="0" aria-valuemax="100" style="width: ' .. (100-sent2rcvd) .. '%;">' .. rcvdLabel .. '</div></div>')
|
|
|
|
else
|
|
print(' ')
|
|
end
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
function percentageBar(total, value, valueLabel)
|
|
-- io.write("****>> "..total.."/"..value.."\n")
|
|
if((total ~= nil) and (total > 0)) then
|
|
pctg = round((value * 100) / total, 0)
|
|
print('<div class="progress"><div class="progress-bar bg-warning" aria-valuenow="'.. pctg..'" aria-valuemin="0" aria-valuemax="100" style="width: ' .. pctg.. '%;">'..valueLabel)
|
|
print('</div></div>')
|
|
else
|
|
print(' ')
|
|
end
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
function makeProgressBar(percentage)
|
|
-- nan check
|
|
if percentage ~= percentage then
|
|
return ""
|
|
end
|
|
|
|
local perc_int = round(percentage)
|
|
return '<span style="width: 70%; float:left"><div class="progress"><div class="progress-bar bg-warning" aria-valuenow="'..
|
|
perc_int ..'" aria-valuemin="0" aria-valuemax="100" style="width: '.. perc_int ..'%;"></div></div></span><span style="width: 30%; margin-left: 15px;">'..
|
|
round(percentage, 1) ..' %</span>'
|
|
end
|
|
|
|
|
|
-- ########################################################
|
|
|
|
--! @brief Prints stacked progress bars with a legend
|
|
--! @total the raw total value (associated to full bar width)
|
|
--! @param bars a table with elements in the following format:
|
|
--! - title: the item legend title
|
|
--! - value: the item raw value
|
|
--! - class: the bootstrap color class, usually: "default", "info", "danger", "warning", "success"
|
|
--! @param other_label optional name for the "other" part of the bar. If nil, it will not be shown.
|
|
--! @param formatter an optional item value formatter
|
|
--! @param css_class an optional css class to apply to the progress div
|
|
--! @return html for the bar
|
|
function stackedProgressBars(total, bars, other_label, formatter, css_class)
|
|
local res = {}
|
|
local cumulative = 0
|
|
local cumulative_perc = 0
|
|
formatter = formatter or (function(x) return x end)
|
|
|
|
-- The bars
|
|
res[#res + 1] = [[<div class=' ]] .. (css_class or "ntop-progress-stacked") .. [['><div class="progress">]]
|
|
|
|
for _, bar in ipairs(bars) do cumulative = cumulative + bar.value end
|
|
if cumulative > total then total = cumulative end
|
|
|
|
for _, bar in ipairs(bars) do
|
|
local percentage = round(bar.value * 100 / total, 2)
|
|
if cumulative_perc + percentage > 100 then percentage = 100 - cumulative_perc end
|
|
cumulative_perc = cumulative_perc + percentage
|
|
if bar.class == nil then bar.class = "primary" end
|
|
if bar.style == nil then bar.style = "" end
|
|
if bar.link ~= nil then res[#res + 1] = [[<a href="]] .. bar.link .. [[">]] end
|
|
res[#res + 1] = [[
|
|
<div class="progress-bar bg-]] .. (bar.class) .. [[" role="progressbar" style="width:]] .. percentage .. [[%;]] .. bar.style .. [["></div></a>]]
|
|
if bar.link ~= nil then res[#res + 1] = [[</a>]] end
|
|
end
|
|
|
|
res[#res + 1] = [[
|
|
</div></div>]]
|
|
|
|
-- The legend
|
|
res[#res + 1] = [[<div class="ntop-progress-stacked-legend">]]
|
|
|
|
local legend_items = bars
|
|
|
|
if other_label ~= nil then
|
|
legend_items = table.clone(bars)
|
|
|
|
legend_items[#legend_items + 1] = {
|
|
title = other_label,
|
|
class = "empty",
|
|
style = "",
|
|
value = math.max(total - cumulative, 0),
|
|
}
|
|
end
|
|
|
|
num = 0
|
|
for _, bar in ipairs(legend_items) do
|
|
res[#res + 1] = [[<span>]]
|
|
if(num > 0) then res[#res + 1] = [[<br>]] end
|
|
if bar.link ~= nil then res[#res + 1] = [[<a href="]] .. bar.link .. [[">]] end
|
|
res[#res + 1] = [[<span class="badge badge-]].. (bar.class) ..[[" style="]] .. bar.style .. [["> </span>]]
|
|
if bar.link ~= nil then res[#res + 1] = [[</a>]] end
|
|
res[#res + 1] = [[<span> ]] .. bar.title .. " (".. formatter(bar.value) ..")</span></span>"
|
|
num = num + 1
|
|
end
|
|
|
|
res[#res + 1] = [[<span style="margin-left: 0"><span></span><span> - ]] .. i18n("total") .. ": ".. formatter(total) .."</span></span>"
|
|
|
|
return table.concat(res)
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
-- label, relative_difference, seconds
|
|
zoom_vals = {
|
|
{ "1m", "now-60s", 60},
|
|
{ "5m", "now-300s", 60*5},
|
|
{ "30m", "now-1800s", 60*30},
|
|
{ "1h", "now-1h", 60*60*1},
|
|
--{ "3h", "now-3h", 60*60*3},
|
|
--{ "6h", "now-6h", 60*60*6},
|
|
--{ "12h", "now-12h", 60*60*12},
|
|
{ "1d", "now-1d", 60*60*24},
|
|
{ "1w", "now-1w", 60*60*24*7},
|
|
--{ "2w", "now-2w", 60*60*24*14},
|
|
{ "1M", "now-1mon", 60*60*24*31},
|
|
--{ "6M", "now-6mon", 60*60*24*31*6},
|
|
{ "1Y", "now-1y", 60*60*24*366}
|
|
}
|
|
|
|
function getZoomAtPos(cur_zoom, pos_offset)
|
|
local pos = 1
|
|
local new_zoom_level = cur_zoom
|
|
for k,v in pairs(zoom_vals) do
|
|
if(zoom_vals[k][1] == cur_zoom) then
|
|
if (pos+pos_offset >= 1 and pos+pos_offset < table.len(zoom_vals)) then
|
|
new_zoom_level = zoom_vals[pos+pos_offset][1]
|
|
break
|
|
end
|
|
end
|
|
pos = pos + 1
|
|
end
|
|
return new_zoom_level
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
function getZoomDuration(cur_zoom)
|
|
for k,v in pairs(zoom_vals) do
|
|
if(zoom_vals[k][1] == cur_zoom) then
|
|
return(zoom_vals[k][3])
|
|
end
|
|
end
|
|
|
|
return(180)
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
local function getEntryStep(schema_name)
|
|
if(starts(schema_name, "custom:") and (getCustomSchemaStep ~= nil)) then
|
|
return(getCustomSchemaStep(schema_name))
|
|
end
|
|
|
|
if(starts(schema_name, "top:")) then
|
|
schema_name = split(schema_name, "top:")[2]
|
|
end
|
|
|
|
local schema_obj = ts_utils.getSchema(schema_name)
|
|
|
|
if(schema_obj) then
|
|
return(schema_obj.options.step)
|
|
end
|
|
|
|
return(nil)
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
local graph_menu_entries = {}
|
|
|
|
-- Menu entries are either populated by printSeries (optimized) or directly by
|
|
-- calling this function. In the latter case it is mandatory to check that the
|
|
-- series actually exist before calling this function.
|
|
--
|
|
-- The rule which determines how an entry is show is:
|
|
-- - If no timeseries exist at all for the entry, the entry will not be shown
|
|
-- - If the visualized interval is less then the entry timseries step, then
|
|
-- the entry will be shown but will be grayed out (disabled state)
|
|
-- - If timeseries exist for the entry in the visualized interval, the
|
|
-- entry will be shown and will be clickable
|
|
function populateGraphMenuEntry(label, base_url, params, tab_id, needs_separator, separator_label, pending, extra_params, serie)
|
|
local url = getPageUrl(base_url, params)
|
|
local step = nil
|
|
|
|
local entry_params = table.clone(params)
|
|
for k, v in pairs(splitUrl(base_url).params) do
|
|
entry_params[k] = v
|
|
end
|
|
|
|
if(params.ts_schema ~= nil) then
|
|
step = getEntryStep(params.ts_schema)
|
|
end
|
|
|
|
local entry = {
|
|
label = label,
|
|
schema = params.ts_schema,
|
|
params = entry_params, -- for graphMenuGetActive
|
|
url = url,
|
|
tab_id = tab_id,
|
|
needs_separator = needs_separator,
|
|
separator_label = separator_label,
|
|
pending = pending, -- true for batched operations
|
|
step = step,
|
|
extra_params = extra_params,
|
|
graph_options = serie,
|
|
}
|
|
|
|
graph_menu_entries[#graph_menu_entries + 1] = entry
|
|
return entry
|
|
end
|
|
|
|
function makeMenuDivider()
|
|
return '<div class="dropdown-divider"></div>'
|
|
end
|
|
|
|
function makeMenuHeader(label)
|
|
return '<li class="dropdown-header">'.. label ..'</li>'
|
|
end
|
|
|
|
function graphMenuDivider()
|
|
graph_menu_entries[#graph_menu_entries + 1] = {html=makeMenuDivider()}
|
|
end
|
|
|
|
function graphMenuHeader(label)
|
|
graph_menu_entries[#graph_menu_entries + 1] = {html=makeMenuHeader(label)}
|
|
end
|
|
|
|
function graphMenuGetActive(schema, params)
|
|
-- These tags are used to determine the active timeseries entry
|
|
local match_tags = {ts_schema=1, ts_query=1, protocol=1, category=1, snmp_port_idx=1, exporter_ifname=1, l4proto=1, command=1}
|
|
|
|
for _, entry in pairs(graph_menu_entries) do
|
|
local extra_params = entry.extra_params or {}
|
|
|
|
if entry.schema == schema and entry.params then
|
|
for k, v in pairs(params) do
|
|
if (match_tags[k] or extra_params[k]) and tostring(entry.params[k]) ~= tostring(v) then
|
|
goto continue
|
|
end
|
|
end
|
|
|
|
return entry
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function printEntry(idx, entry)
|
|
local parts = {}
|
|
|
|
parts[#parts + 1] = [[<a class='dropdown-item' href="]] .. entry.url .. [[" ]]
|
|
|
|
if not isEmptyString(entry.tab_id) then
|
|
parts[#parts + 1] = [[id="]] .. entry.tab_id .. [[" ]]
|
|
end
|
|
|
|
parts[#parts + 1] = [[> ]] .. entry.label .. [[</a>]]
|
|
|
|
print(table.concat(parts, ""))
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
local function ignoreEntry(entry)
|
|
return(entry.pending and (entry.pending > 0))
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
-- Prints the menu from the populated graph_menu_entries.
|
|
-- The entry_print_callback is called to print the actual entries.
|
|
function printGraphMenuEntries(entry_print_callback, active_entry, start_time, end_time)
|
|
local active_entries = {}
|
|
local active_idx = 1 -- index in active_entries
|
|
local needs_separator = false
|
|
local separator_label = nil
|
|
local tdiff = (end_time - start_time)
|
|
|
|
for _, entry in ipairs(graph_menu_entries) do
|
|
if active_idx ~= 1 then
|
|
needs_separator = needs_separator or entry.needs_separator
|
|
separator_label = separator_label or entry.separator_label
|
|
end
|
|
|
|
if(entry.step) then
|
|
entry.disabled = (tdiff <= entry.step)
|
|
end
|
|
|
|
if(active_entry == entry) then
|
|
-- Always consider the selected entry as active
|
|
entry.pending = 0
|
|
end
|
|
|
|
if(ignoreEntry(entry)) then
|
|
-- not verified, act like it does not exist
|
|
goto continue
|
|
end
|
|
|
|
if(needs_separator) then
|
|
print(makeMenuDivider())
|
|
needs_separator = false
|
|
end
|
|
if(separator_label) then
|
|
print(makeMenuHeader(separator_label))
|
|
separator_label = nil
|
|
end
|
|
|
|
if entry.html then
|
|
print(entry.html)
|
|
else
|
|
entry_print_callback(active_idx, entry)
|
|
active_entries[#active_entries + 1] = entry
|
|
active_idx = active_idx + 1
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
|
|
-- NOTE: only return the graph_menu_entries which are non-pending
|
|
return active_entries
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
-- To be called after the menu has been populated. Returns the
|
|
-- min step of the entries.
|
|
function getMinGraphEntriesStep()
|
|
local min_step = nil
|
|
|
|
for _, entry in pairs(graph_menu_entries) do
|
|
if(not ignoreEntry(entry) and (entry.step)) then
|
|
if(min_step == nil) then
|
|
min_step = entry.step
|
|
else
|
|
min_step = math.min(entry.step, min_step)
|
|
end
|
|
end
|
|
end
|
|
|
|
return(min_step)
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
function printSeries(options, tags, start_time, end_time, base_url, params)
|
|
local series = options.timeseries
|
|
local needs_separator = false
|
|
local separator_label = nil
|
|
local batch_id_to_entry = {}
|
|
local device_timeseries_mac = options.device_timeseries_mac
|
|
local mac_tags = nil
|
|
local mac_params = nil
|
|
local mac_baseurl = ntop.getHttpPrefix() .. "/lua/mac_details.lua?page=historical"
|
|
local is_pro = ntop.isPro()
|
|
local is_enterprise = ntop.isEnterprise()
|
|
local tdiff = (end_time - start_time)
|
|
|
|
if params.tskey then
|
|
-- this can contain a MAC address for local broadcast domain hosts
|
|
tags = table.clone(tags)
|
|
tags.host = params.tskey
|
|
end
|
|
|
|
if(device_timeseries_mac ~= nil) then
|
|
mac_tags = table.clone(tags)
|
|
mac_tags.host = nil
|
|
mac_tags.mac = device_timeseries_mac
|
|
mac_params = table.clone(params)
|
|
mac_params.host = device_timeseries_mac
|
|
end
|
|
|
|
for _, serie in ipairs(series) do
|
|
if ((have_nedge and serie.nedge_exclude) or (not have_nedge and serie.nedge_only)) or
|
|
(serie.pro_skip and is_pro) or
|
|
(serie.enterprise_only and (not is_enterprise)) then
|
|
goto continue
|
|
end
|
|
|
|
local query_start = start_time
|
|
|
|
if(serie.schema ~= nil) then
|
|
local step = getEntryStep(serie.schema)
|
|
|
|
if step and (tdiff <= step) then
|
|
-- This entry will not be clickable but maybe it will be
|
|
-- shown in disabled state if any data for it exists, so
|
|
-- remove the time constraint
|
|
query_start = 0
|
|
end
|
|
end
|
|
|
|
if serie.separator then
|
|
needs_separator = true
|
|
separator_label = serie.label
|
|
else
|
|
local k = serie.schema
|
|
local v = serie.label
|
|
local exists = false
|
|
local entry_tags = tags
|
|
local entry_params = table.merge(params, serie.extra_params)
|
|
local entry_baseurl = base_url
|
|
local override_link = nil
|
|
|
|
-- Contains the list of batch_ids to be associated to this menu entry.
|
|
-- The entry can only be shown when all the batch_ids have been confirmed
|
|
-- in getBatchedListSeriesResult
|
|
local batch_ids = {}
|
|
|
|
if starts(k, "custom:") then
|
|
if not ntop.isPro() then
|
|
goto continue
|
|
end
|
|
|
|
-- exists by default, otherwise specify a serie.check below
|
|
exists = true
|
|
end
|
|
|
|
local to_check = serie.check or (serie.custom_schema and serie.custom_schema.bases)
|
|
|
|
if(to_check ~= nil) then
|
|
exists = true
|
|
|
|
-- In the case of custom series, the serie can only be shown if all
|
|
-- the component series exists
|
|
for idx, serie in pairs(to_check) do
|
|
local exist_tags = tags
|
|
|
|
if starts(k, "custom:") then
|
|
exist_tags = getCustomSchemaTags(k, exist_tags, idx)
|
|
end
|
|
|
|
local batch_id = ts_utils.batchListSeries(serie, table.merge(exist_tags, serie.extra_params), query_start)
|
|
|
|
if batch_id == nil then
|
|
exists = false
|
|
break
|
|
end
|
|
|
|
batch_ids[#batch_ids +1] = batch_id
|
|
end
|
|
elseif not exists then
|
|
if(mac_tags ~= nil) and (starts(k, "mac:")) then
|
|
-- This is a mac timeseries shown under the host
|
|
entry_tags = mac_tags
|
|
entry_params = mac_params
|
|
entry_baseurl = mac_baseurl
|
|
end
|
|
|
|
-- only show if there has been an update within the specified time frame
|
|
local batch_id = ts_utils.batchListSeries(k, table.merge(entry_tags, serie.extra_params), query_start)
|
|
|
|
if batch_id ~= nil then
|
|
-- assume it exists for now, will verify in getBatchedListSeriesResult
|
|
exists = true
|
|
batch_ids[#batch_ids +1] = batch_id
|
|
end
|
|
end
|
|
|
|
if exists then
|
|
local entry = populateGraphMenuEntry(v, entry_baseurl, table.merge(entry_params, {ts_schema=k}), nil,
|
|
needs_separator, separator_label, #batch_ids --[[ pending ]], serie.extra_params, serie)
|
|
|
|
if entry then
|
|
for _, batch_id in pairs(batch_ids) do
|
|
batch_id_to_entry[batch_id] = entry
|
|
end
|
|
end
|
|
|
|
needs_separator = false
|
|
separator_label = nil
|
|
end
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
|
|
-- nDPI applications
|
|
if options.top_protocols then
|
|
local schema = split(options.top_protocols, "top:")[2]
|
|
local proto_tags = table.clone(tags)
|
|
proto_tags.protocol = nil
|
|
|
|
local series = ts_utils.listSeries(schema, proto_tags, start_time)
|
|
|
|
if not table.empty(series) then
|
|
graphMenuDivider()
|
|
graphMenuHeader(i18n("applications"))
|
|
|
|
local by_protocol = {}
|
|
|
|
for _, serie in pairs(series) do
|
|
by_protocol[serie.protocol] = 1
|
|
end
|
|
|
|
for protocol in pairsByKeys(by_protocol, asc) do
|
|
local proto_id = protocol
|
|
populateGraphMenuEntry(protocol, base_url, table.merge(params, {ts_schema=schema, protocol=proto_id}))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- L4 protocols
|
|
if options.l4_protocols then
|
|
local schema = options.l4_protocols
|
|
local l4_tags = table.clone(tags)
|
|
l4_tags.l4proto = nil
|
|
|
|
local series = ts_utils.listSeries(schema, l4_tags, start_time)
|
|
|
|
if not table.empty(series) then
|
|
graphMenuDivider()
|
|
graphMenuHeader(i18n("protocols"))
|
|
|
|
local by_protocol = {}
|
|
|
|
for _, serie in pairs(series) do
|
|
local sortkey = serie.l4proto
|
|
|
|
if sortkey == "other_ip" then
|
|
-- place at the end
|
|
sortkey = "z" .. sortkey
|
|
end
|
|
|
|
by_protocol[sortkey] = serie.l4proto
|
|
end
|
|
|
|
for _, protocol in pairsByKeys(by_protocol, asc) do
|
|
local proto_id = protocol
|
|
local label
|
|
|
|
if proto_id == "other_ip" then
|
|
label = i18n("other")
|
|
else
|
|
label = string.upper(protocol)
|
|
end
|
|
|
|
populateGraphMenuEntry(label, base_url, table.merge(params, {ts_schema=schema, l4proto=proto_id}))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- nDPI application categories
|
|
if options.top_categories then
|
|
local schema = split(options.top_categories, "top:")[2]
|
|
local cat_tags = table.clone(tags)
|
|
cat_tags.category = nil
|
|
local series = ts_utils.listSeries(schema, cat_tags, start_time)
|
|
|
|
if not table.empty(series) then
|
|
graphMenuDivider()
|
|
graphMenuHeader(i18n("categories"))
|
|
|
|
local by_category = {}
|
|
|
|
for _, serie in pairs(series) do
|
|
by_category[serie.category] = 1
|
|
end
|
|
|
|
for category in pairsByKeys(by_category, asc) do
|
|
populateGraphMenuEntry(category, base_url, table.merge(params, {ts_schema=schema, category=category}))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Perform the batched operations
|
|
local result = ts_utils.getBatchedListSeriesResult()
|
|
|
|
for batch_id, res in pairs(result) do
|
|
local entry = batch_id_to_entry[batch_id]
|
|
|
|
if entry and not table.empty(res) and entry.pending then
|
|
-- entry exists, decrement the number of pending requests
|
|
entry.pending = entry.pending - 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
function getMinZoomResolution(schema)
|
|
local schema_obj = ts_utils.getSchema(schema)
|
|
|
|
if schema_obj then
|
|
if schema_obj.options.step >= 300 then
|
|
return '30m'
|
|
elseif schema_obj.options.step >= 60 then
|
|
return '5m'
|
|
end
|
|
end
|
|
|
|
return '1m'
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
function printNotes(notes_items)
|
|
print("<b>" .. i18n("notes").. "</b><ul>")
|
|
|
|
for _, note in ipairs(notes_items) do
|
|
print("<li>" ..note .. "</li>")
|
|
end
|
|
|
|
print("</ul>")
|
|
end
|
|
|
|
-- ########################################################
|
|
|
|
function drawGraphs(ifid, schema, tags, zoomLevel, baseurl, selectedEpoch, options)
|
|
local debug_rrd = false
|
|
options = options or {}
|
|
|
|
if((selectedEpoch == nil) or (selectedEpoch == "")) then
|
|
-- Refresh the page every minute unless:
|
|
-- ** a specific epoch has been selected or
|
|
-- ** the user is browsing historical top talkers and protocols
|
|
print[[
|
|
<script>
|
|
setInterval(function() {
|
|
var talkers_loaded, protocols_loaded, flows_loaded;
|
|
if($('a[href="#historical-top-talkers"]').length){
|
|
talkers_loaded = $('a[href="#historical-top-talkers"]').attr("loaded");
|
|
}
|
|
if($('a[href="#historical-top-apps"]').length){
|
|
protocols_loaded = $('a[href="#historical-top-apps"]').attr("loaded");
|
|
}
|
|
if($('a[href="#historical-flows"]').length){
|
|
flows_loaded = $('a[href="#historical-flows"]').attr("loaded");
|
|
}
|
|
if(typeof talkers_loaded == 'undefined'
|
|
&& typeof protocols_loaded == 'undefined'
|
|
&& typeof flows_loaded == 'undefined'){
|
|
]] if not ntop.isPro() then print[[
|
|
window.location.reload(); /* do not reload, it's annoying */
|
|
]]
|
|
end
|
|
print[[
|
|
}
|
|
}, 60*1000);
|
|
</script>]]
|
|
end
|
|
|
|
local min_zoom = getMinZoomResolution(schema)
|
|
local min_zoom_k = 1
|
|
if(zoomLevel == nil) then zoomLevel = min_zoom end
|
|
|
|
if ntop.isPro() then
|
|
_ifstats = interface.getStats()
|
|
drawProGraph(ifid, schema, tags, zoomLevel, baseurl, options)
|
|
return
|
|
end
|
|
|
|
nextZoomLevel = zoomLevel;
|
|
epoch = tonumber(selectedEpoch);
|
|
|
|
for k,v in ipairs(zoom_vals) do
|
|
if zoom_vals[k][1] == min_zoom then
|
|
min_zoom_k = k
|
|
end
|
|
|
|
if(zoom_vals[k][1] == zoomLevel) then
|
|
if(k > 1) then
|
|
nextZoomLevel = zoom_vals[math.max(k-1, min_zoom_k)][1]
|
|
end
|
|
if(epoch ~= nil) then
|
|
start_time = epoch - math.floor(zoom_vals[k][3] / 2)
|
|
end_time = epoch + math.floor(zoom_vals[k][3] / 2)
|
|
else
|
|
end_time = os.time()
|
|
start_time = end_time - zoom_vals[k][3]
|
|
end
|
|
end
|
|
end
|
|
|
|
if options.tskey then
|
|
-- this can contain a MAC address for local broadcast domain hosts
|
|
tags = table.clone(tags)
|
|
tags.host = options.tskey
|
|
end
|
|
|
|
local data = ts_utils.query(schema, tags, start_time, end_time)
|
|
|
|
if(data) then
|
|
print [[
|
|
|
|
<style>
|
|
#chart_container {
|
|
display: inline-block;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
}
|
|
#chart {
|
|
float: left;
|
|
}
|
|
#legend {
|
|
float: left;
|
|
margin-left: 15px;
|
|
color: black;
|
|
background: white;
|
|
}
|
|
#y_axis {
|
|
float: left;
|
|
width: 40px;
|
|
}
|
|
|
|
</style>
|
|
|
|
<div>
|
|
|
|
<div class="container-fluid">
|
|
<ul class="nav nav-tabs" role="tablist" id="historical-tabs-container">
|
|
<li class="nav-item active"> <a class="nav-link active" href="#historical-tab-chart" role="tab" data-toggle="tab"> Chart </a> </li>
|
|
]]
|
|
|
|
local show_historical_tabs = ntop.getPrefs().is_dump_flows_to_mysql_enabled and options.show_historical
|
|
|
|
if show_historical_tabs then
|
|
print('<li class="nav-item"><a class="nav-link" href="#historical-flows" role="tab" data-toggle="tab" id="tab-flows-summary"> Flows </a> </li>\n')
|
|
end
|
|
|
|
print[[
|
|
</ul>
|
|
|
|
|
|
<div class="tab-content">
|
|
<div class="tab-pane active in" id="historical-tab-chart">
|
|
|
|
<br>
|
|
<table border=0>
|
|
<tr><td valign="top">
|
|
]]
|
|
|
|
local page_params = {
|
|
ts_schema = schema,
|
|
zoom = zoomLevel or '',
|
|
epoch = selectedEpoch or '',
|
|
tskey = options.tskey,
|
|
}
|
|
|
|
if(options.timeseries) then
|
|
print [[
|
|
<div class="dropdown d-inline">
|
|
<button class="btn btn-light btn-sm dropdown-toggle" data-toggle="dropdown">Timeseries <span class="caret"></span></button>
|
|
<div class="dropdown-menu responsive-dropdown">
|
|
]]
|
|
|
|
printSeries(options, tags, start_time, end_time, baseurl, page_params)
|
|
printGraphMenuEntries(printEntry, nil, start_time, end_time)
|
|
|
|
print [[
|
|
</div>
|
|
</div><!-- /btn-group -->
|
|
]]
|
|
end -- options.timeseries
|
|
|
|
print(' Timeframe: <div class="btn-group btn-group-toggle" data-toggle="buttons" id="graph_zoom">\n')
|
|
|
|
for k,v in ipairs(zoom_vals) do
|
|
-- display 1 minute button only for networks and interface stats
|
|
-- but exclude applications. Application statistics are gathered
|
|
-- every 5 minutes
|
|
if zoom_vals[k][1] == '1m' and min_zoom ~= '1m' then
|
|
goto continue
|
|
elseif zoom_vals[k][1] == '5m' and min_zoom ~= '1m' and min_zoom ~= '5m' then
|
|
goto continue
|
|
end
|
|
print('<label class="btn btn-link ')
|
|
|
|
if(zoom_vals[k][1] == zoomLevel) then
|
|
print("active")
|
|
end
|
|
print('">')
|
|
|
|
local params = table.merge(page_params, {zoom=zoom_vals[k][1]})
|
|
|
|
-- Additional parameters
|
|
if tags.protocol ~= nil then
|
|
params["protocol"] = tags.protocol
|
|
end
|
|
if tags.category ~= nil then
|
|
params["category"] = tags.category
|
|
end
|
|
|
|
local url = getPageUrl(baseurl, params)
|
|
|
|
print('<input type="radio" name="options" id="zoom_level_'..k..'" value="'..url..'">'.. zoom_vals[k][1] ..'</input></label>\n')
|
|
::continue::
|
|
end
|
|
|
|
print [[
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
$('input:radio[id^=zoom_level_]').change( function() {
|
|
window.open(this.value,'_self',false);
|
|
});
|
|
</script>
|
|
|
|
<br />
|
|
<p>
|
|
|
|
|
|
<div id="legend"></div>
|
|
<div id="chart_legend"></div>
|
|
<div id="chart" style="margin-right: 50px; margin-left: 10px; display: table-cell"></div>
|
|
<p><font color=lightgray><small>NOTE: Click on the graph to zoom.</small></font>
|
|
|
|
</td>
|
|
|
|
|
|
<td rowspan=2>
|
|
<div id="y_axis"></div>
|
|
|
|
<div style="margin-left: 10px; display: table">
|
|
<div id="chart_container" style="display: table-row">
|
|
|
|
]]
|
|
|
|
local format_as_bps = true
|
|
local formatter_fctn
|
|
local label = data.series[1].label
|
|
|
|
if string.contains(label, "packets") or string.contains(label, "flows") or label:starts("num_") then
|
|
format_as_bps = false
|
|
formatter_fctn = "fint"
|
|
else
|
|
formatter_fctn = "fbits"
|
|
end
|
|
|
|
print [[
|
|
<table class="table table-bordered table-striped" style="border: 0; margin-right: 10px; display: table-cell">
|
|
]]
|
|
|
|
print(' <tr><th> </th><th>Time</th><th>Value</th></tr>\n')
|
|
|
|
local stats = data.statistics
|
|
|
|
if(stats ~= nil) then
|
|
local minval_time = stats.min_val_idx and (data.start + data.step * stats.min_val_idx) or ""
|
|
local maxval_time = stats.max_val_idx and (data.start + data.step * stats.max_val_idx) or ""
|
|
local lastval_time = data.start + data.step * (data.count-1)
|
|
local lastval = 0
|
|
|
|
for _, serie in pairs(data.series) do
|
|
lastval = lastval + serie.data[data.count]
|
|
end
|
|
if(not format_as_bps) then
|
|
print(' <tr><th>Min</th><td>' .. os.date("%x %X", minval_time) .. '</td><td>' .. formatValue(stats.min_val or "") .. '</td></tr>\n')
|
|
print(' <tr><th>Max</th><td>' .. os.date("%x %X", maxval_time) .. '</td><td>' .. formatValue(stats.max_val or "") .. '</td></tr>\n')
|
|
print(' <tr><th>Last</th><td>' .. os.date("%x %X", lastval_time) .. '</td><td>' .. formatValue(round(lastval), 1) .. '</td></tr>\n')
|
|
print(' <tr><th>Average</th><td colspan=2>' .. formatValue(round(stats.average, 2)) .. '</td></tr>\n')
|
|
print(' <tr><th>95th <A HREF=https://en.wikipedia.org/wiki/Percentile>Percentile</A></th><td colspan=2>' .. formatValue(round(stats["95th_percentile"], 2)) .. '</td></tr>\n')
|
|
print(' <tr><th>Total Number</th><td colspan=2>' .. formatValue(round(stats.total)) .. '</td></tr>\n')
|
|
else
|
|
print(' <tr><th>Min</th><td>' .. os.date("%x %X", minval_time) .. '</td><td>' .. bitsToSize((stats.min_val*8) or "") .. '</td></tr>\n')
|
|
print(' <tr><th>Max</th><td>' .. os.date("%x %X", maxval_time) .. '</td><td>' .. bitsToSize((stats.max_val*8) or "") .. '</td></tr>\n')
|
|
print(' <tr><th>Last</th><td>' .. os.date("%x %X", lastval_time) .. '</td><td>' .. bitsToSize(lastval*8) .. '</td></tr>\n')
|
|
print(' <tr><th>Average</th><td colspan=2>' .. bitsToSize(stats.average*8) .. '</td></tr>\n')
|
|
print(' <tr><th>95th <A HREF=https://en.wikipedia.org/wiki/Percentile>Percentile</A></th><td colspan=2>' .. bitsToSize(stats["95th_percentile"]*8) .. '</td></tr>\n')
|
|
print(' <tr><th>Total Traffic</th><td colspan=2>' .. bytesToSize(stats.total) .. '</td></tr>\n')
|
|
end
|
|
end
|
|
|
|
print(' <tr><th>Selection Time</th><td colspan=2><div id=when></div></td></tr>\n')
|
|
|
|
if top_talkers_utils.areTopEnabled(ifid) then
|
|
print(' <tr><th>Minute<br>Interface<br>Top Talkers</th><td colspan=2><div id=talkers></div></td></tr>\n')
|
|
end
|
|
|
|
|
|
print [[
|
|
</table>
|
|
]]
|
|
|
|
print[[</div></td></tr></table>
|
|
|
|
</div> <!-- closes div id "historical-tab-chart "-->
|
|
]]
|
|
|
|
if show_historical_tabs then
|
|
local host = tags.host -- can be nil
|
|
local l7proto = tags.protocol or ""
|
|
local k2info = hostkey2hostinfo(host)
|
|
|
|
print('<div class="tab-pane" id="historical-flows">')
|
|
if tonumber(start_time) ~= nil and tonumber(end_time) ~= nil then
|
|
-- if both start_time and end_time are vaid epoch we can print finer-grained top flows
|
|
historicalFlowsTab(ifid, k2info["host"] or '', start_time, end_time, l7proto, '', '', '', k2info["vlan"])
|
|
else
|
|
printGraphTopFlows(ifid, k2info["host"] or '', _GET["epoch"], zoomLevel, l7proto, k2info["vlan"])
|
|
end
|
|
print('</div>')
|
|
end
|
|
|
|
print[[
|
|
</div> <!-- closes div class "tab-content" -->
|
|
</div> <!-- closes div class "container-fluid" -->
|
|
|
|
<script>
|
|
|
|
var palette = new Rickshaw.Color.Palette();
|
|
|
|
var graph = new Rickshaw.Graph( {
|
|
element: document.getElementById("chart"),
|
|
width: 600,
|
|
height: 300,
|
|
renderer: 'area',
|
|
series: [
|
|
]]
|
|
|
|
for serie_idx, serie in ipairs(data.series) do
|
|
print("{name: \"" .. serie.label .. "\"")
|
|
print("\n, color: '".. graph_colors[serie_idx] .."', data: [")
|
|
|
|
local t = data.start
|
|
|
|
for i, val in ipairs(serie.data) do
|
|
print("{x: " .. t)
|
|
if (format_as_bps) then
|
|
print(",y: " .. (val*8) .. "},\n")
|
|
else
|
|
print(",y: " .. val .. "},\n")
|
|
end
|
|
t = t + data.step
|
|
end
|
|
|
|
print("]},")
|
|
end
|
|
|
|
print [[
|
|
]} );
|
|
|
|
graph.render();
|
|
|
|
var chart_legend = document.querySelector('#chart_legend');
|
|
|
|
|
|
function fdate(when) {
|
|
var epoch = when*1000;
|
|
var d = new Date(epoch);
|
|
|
|
return(d);
|
|
}
|
|
|
|
var Hover = Rickshaw.Class.create(Rickshaw.Graph.HoverDetail, {
|
|
graph: graph,
|
|
xFormatter: function(x) { return new Date( x * 1000 ); },
|
|
yFormatter: function(bits) { return(]] print(formatter_fctn) print [[(bits)); },
|
|
render: function(args) {
|
|
var graph = this.graph;
|
|
var points = args.points;
|
|
var point = points.filter( function(p) { return p.active } ).shift();
|
|
|
|
if(point.value.y === null) return;
|
|
|
|
var formattedXValue = fdate(point.value.x); // point.formattedXValue;
|
|
var formattedYValue = ]]
|
|
print(formatter_fctn)
|
|
print [[(point.value.y); // point.formattedYValue;
|
|
var infoHTML = "";
|
|
]]
|
|
|
|
print[[
|
|
|
|
infoHTML += "<ul>";
|
|
$.ajax({
|
|
type: 'GET',
|
|
url: ']]
|
|
print(ntop.getHttpPrefix().."/lua/get_top_talkers.lua?epoch='+point.value.x+'&addvlan=true")
|
|
print [[',
|
|
data: { epoch: point.value.x },
|
|
async: false,
|
|
success: function(content) {
|
|
var info = jQuery.parseJSON(content);
|
|
$.each(info, function(i, n) {
|
|
if (n.length > 0)
|
|
infoHTML += "<li>"+capitaliseFirstLetter(i)+" [Avg Traffic/sec]<ol>";
|
|
var items = 0;
|
|
var other_traffic = 0;
|
|
$.each(n, function(j, m) {
|
|
if((items < 3) && (m.address != "Other")) {
|
|
infoHTML += "<li><a href='host_details.lua?host="+m.address+"'>"+abbreviateString(m.label ? m.label : m.address,24);
|
|
infoHTML += "</a>";
|
|
if (m.vlan != "0") infoHTML += " ("+m.vlanm+")";
|
|
infoHTML += " ("+fbits((m.value*8)/60)+")</li>";
|
|
items++;
|
|
} else
|
|
other_traffic += m.value;
|
|
});
|
|
if (other_traffic > 0)
|
|
infoHTML += "<li>Other ("+fbits((other_traffic*8)/60)+")</li>";
|
|
if (n.length > 0)
|
|
infoHTML += "</ol></li>";
|
|
});
|
|
infoHTML += "</ul></li></li>";
|
|
}
|
|
});
|
|
infoHTML += "</ul>";]]
|
|
|
|
print [[
|
|
this.element.innerHTML = '';
|
|
this.element.style.left = graph.x(point.value.x) + 'px';
|
|
|
|
/*var xLabel = document.createElement('div');
|
|
xLabel.setAttribute("style", "opacity: 0.5; background-color: #EEEEEE; filter: alpha(opacity=0.5)");
|
|
xLabel.className = 'x_label';
|
|
xLabel.innerHTML = formattedXValue + infoHTML;
|
|
this.element.appendChild(xLabel);
|
|
*/
|
|
$('#when').html(formattedXValue);
|
|
$('#talkers').html(infoHTML);
|
|
|
|
|
|
var item = document.createElement('div');
|
|
|
|
item.className = 'item';
|
|
item.innerHTML = this.formatter(point.series, point.value.x, point.value.y, formattedXValue, formattedYValue, point);
|
|
item.style.top = this.graph.y(point.value.y0 + point.value.y) + 'px';
|
|
this.element.appendChild(item);
|
|
|
|
var dot = document.createElement('div');
|
|
dot.className = 'dot';
|
|
dot.style.top = item.style.top;
|
|
dot.style.borderColor = point.series.color;
|
|
this.element.appendChild(dot);
|
|
|
|
if(point.active) {
|
|
item.className = 'item active';
|
|
dot.className = 'dot active';
|
|
}
|
|
|
|
this.show();
|
|
|
|
if(typeof this.onRender == 'function') {
|
|
this.onRender(args);
|
|
}
|
|
|
|
// Put the selected graph epoch into the legend
|
|
//chart_legend.innerHTML = point.value.x; // Epoch
|
|
|
|
this.selected_epoch = point.value.x;
|
|
|
|
//event
|
|
}
|
|
} );
|
|
|
|
var hover = new Hover( { graph: graph } );
|
|
|
|
var legend = new Rickshaw.Graph.Legend( {
|
|
graph: graph,
|
|
element: document.getElementById('legend')
|
|
} );
|
|
|
|
//var axes = new Rickshaw.Graph.Axis.Time( { graph: graph } ); axes.render();
|
|
|
|
var yAxis = new Rickshaw.Graph.Axis.Y({
|
|
graph: graph,
|
|
tickFormat: ]] print(formatter_fctn) print [[
|
|
});
|
|
|
|
yAxis.render();
|
|
|
|
]]
|
|
|
|
if zoomLevel ~= nextZoomLevel then
|
|
print[[
|
|
$("#chart").click(function() {
|
|
if(hover.selected_epoch)
|
|
window.location.href = ']]
|
|
print(baseurl .. '&ts_schema=' .. schema .. '&zoom=' .. nextZoomLevel)
|
|
|
|
if tags.protocol ~= nil then
|
|
print("&protocol=" .. tags.protocol)
|
|
elseif tags.category ~= nil then
|
|
print("&category=" .. tags.category)
|
|
end
|
|
|
|
print('&epoch=')
|
|
print[['+hover.selected_epoch;
|
|
});]]
|
|
end
|
|
|
|
print[[
|
|
</script>
|
|
|
|
]]
|
|
else
|
|
print("<div class=\"alert alert-danger\"><img src=".. ntop.getHttpPrefix() .. "/img/warning.png> No data found</div>")
|
|
end -- if(data)
|
|
end
|
|
|
|
function printGraphTopFlows(ifId, host, epoch, zoomLevel, l7proto, vlan)
|
|
-- Check if the DB is enabled
|
|
rsp = interface.execSQLQuery("show tables")
|
|
if(rsp == nil) then return end
|
|
|
|
if((epoch == nil) or (epoch == "")) then epoch = os.time() end
|
|
|
|
local d = getZoomDuration(zoomLevel)
|
|
|
|
epoch_end = epoch
|
|
epoch_begin = epoch-d
|
|
|
|
historicalFlowsTab(ifId, host, epoch_begin, epoch_end, l7proto, '', '', '', vlan)
|
|
end
|
|
|
|
-- #################################################
|
|
|
|
--
|
|
-- proto table should contain the following information:
|
|
-- string traffic_quota
|
|
-- string time_quota
|
|
-- string protoName
|
|
--
|
|
-- ndpi_stats or category_stats can be nil if they are not relevant for the proto
|
|
--
|
|
-- quotas_to_show can contain:
|
|
-- bool traffic
|
|
-- bool time
|
|
--
|
|
function printProtocolQuota(proto, ndpi_stats, category_stats, quotas_to_show, show_td, hide_limit)
|
|
local total_bytes = 0
|
|
local total_duration = 0
|
|
local output = {}
|
|
|
|
if ndpi_stats ~= nil then
|
|
-- This is a single protocol
|
|
local proto_stats = ndpi_stats[proto.protoName]
|
|
if proto_stats ~= nil then
|
|
total_bytes = proto_stats["bytes.sent"] + proto_stats["bytes.rcvd"]
|
|
total_duration = proto_stats["duration"]
|
|
end
|
|
else
|
|
-- This is a category
|
|
local cat_stats = category_stats[proto.protoName]
|
|
if cat_stats ~= nil then
|
|
total_bytes = cat_stats["bytes"]
|
|
total_duration = cat_stats["duration"]
|
|
end
|
|
end
|
|
|
|
if quotas_to_show.traffic then
|
|
local bytes_exceeded = ((proto.traffic_quota ~= "0") and (total_bytes >= tonumber(proto.traffic_quota)))
|
|
local lb_bytes = bytesToSize(total_bytes)
|
|
local lb_bytes_quota = ternary(proto.traffic_quota ~= "0", bytesToSize(tonumber(proto.traffic_quota)), i18n("unlimited"))
|
|
local traffic_taken = ternary(proto.traffic_quota ~= "0", math.min(total_bytes, tonumber(proto.traffic_quota)), 0)
|
|
local traffic_remaining = math.max(tonumber(proto.traffic_quota) - traffic_taken, 0)
|
|
local traffic_quota_ratio = round(traffic_taken * 100 / (traffic_taken + traffic_remaining), 0) or 0
|
|
if not traffic_quota_ratio then traffic_quota_ratio = 0 end
|
|
|
|
if show_td then
|
|
output[#output + 1] = [[<td class='text-right']]..ternary(bytes_exceeded, ' style=\'color:red;\'', '').."><span>"..lb_bytes..ternary(hide_limit, "", " / "..lb_bytes_quota).."</span>"
|
|
end
|
|
|
|
output[#output + 1] = [[
|
|
<div class='progress' style=']]..(quotas_to_show.traffic_style or "")..[['>
|
|
<div class='progress-bar bg-warning' aria-valuenow=']]..traffic_quota_ratio..'\' aria-valuemin=\'0\' aria-valuemax=\'100\' style=\'width: '..traffic_quota_ratio..'%;\'>'..
|
|
ternary(traffic_quota_ratio == traffic_quota_ratio --[[nan check]], traffic_quota_ratio, 0)..[[%
|
|
</div>
|
|
</div>]]
|
|
if show_td then output[#output + 1] = ("</td>") end
|
|
end
|
|
|
|
if quotas_to_show.time then
|
|
local time_exceeded = ((proto.time_quota ~= "0") and (total_duration >= tonumber(proto.time_quota)))
|
|
local lb_duration = secondsToTime(total_duration)
|
|
local lb_duration_quota = ternary(proto.time_quota ~= "0", secondsToTime(tonumber(proto.time_quota)), i18n("unlimited"))
|
|
|
|
local duration_taken = ternary(proto.time_quota ~= "0", math.min(total_duration, tonumber(proto.time_quota)), 0)
|
|
local duration_remaining = math.max(proto.time_quota - duration_taken, 0)
|
|
local duration_quota_ratio = round(duration_taken * 100 / (duration_taken+duration_remaining), 0) or 0
|
|
|
|
if show_td then
|
|
output[#output + 1] = [[<td class='text-right']]..ternary(time_exceeded, ' style=\'color:red;\'', '').."><span>"..lb_duration..ternary(hide_limit, "", " / "..lb_duration_quota).."</span>"
|
|
end
|
|
|
|
output[#output + 1] = ([[
|
|
<div class='progress' style=']]..(quotas_to_show.time_style or "")..[['>
|
|
<div class='progress-bar bg-warning' aria-valuenow=']]..duration_quota_ratio..'\' aria-valuemin=\'0\' aria-valuemax=\'100\' style=\'width: '..duration_quota_ratio..'%;\'>'..
|
|
ternary(duration_quota_ratio == duration_quota_ratio --[[nan check]], duration_quota_ratio, 0)..[[%
|
|
</div>
|
|
</div>]])
|
|
if show_td then output[#output + 1] = ("</td>") end
|
|
end
|
|
|
|
return table.concat(output, '')
|
|
end
|
|
|
|
-- #################################################
|
|
|
|
function poolDropdown(ifId, pool_id, exclude)
|
|
local output = {}
|
|
exclude = exclude or {}
|
|
|
|
for _,pool in ipairs(host_pools_utils.getPoolsList(ifId)) do
|
|
if (not exclude[pool.id]) or (pool.id == pool_id) then
|
|
output[#output + 1] = '<option value="' .. pool.id .. '"'
|
|
|
|
if pool.id == pool_id then
|
|
output[#output + 1] = ' selected'
|
|
end
|
|
|
|
local limit_reached = false
|
|
|
|
if not ntop.isEnterprise() then
|
|
local n_members = table.len(host_pools_utils.getPoolMembers(ifId, pool.id) or {})
|
|
|
|
if n_members >= host_pools_utils.LIMITED_NUMBER_POOL_MEMBERS then
|
|
limit_reached = true
|
|
end
|
|
end
|
|
|
|
if exclude[pool.id] or limit_reached then
|
|
output[#output + 1] = ' disabled'
|
|
end
|
|
|
|
output[#output + 1] = '>' .. pool.name .. ternary(limit_reached, " ("..i18n("host_pools.members_limit_reached")..")", "") .. '</option>'
|
|
end
|
|
end
|
|
|
|
return table.concat(output, '')
|
|
end
|
|
|
|
-- #################################################
|
|
|
|
function printPoolChangeDropdown(ifId, pool_id, have_nedge)
|
|
local output = {}
|
|
|
|
output[#output + 1] = [[<tr>
|
|
<th>]] .. i18n(ternary(have_nedge, "nedge.user", "host_config.host_pool")) .. [[</th>
|
|
<td>
|
|
<select name="pool" class="form-control" style="width:20em; display:inline;">]]
|
|
|
|
output[#output + 1] = poolDropdown(ifId, pool_id)
|
|
|
|
local edit_pools_link = ternary(have_nedge, "/lua/pro/nedge/admin/nf_list_users.lua", "/lua/if_stats.lua?page=pools#create")
|
|
|
|
output[#output + 1] = [[
|
|
</select>
|
|
<A HREF="]] .. ntop.getHttpPrefix() .. edit_pools_link .. [["><i class="fas fa-sm fa-cog" aria-hidden="true" title="]]
|
|
..i18n(ternary(have_nedge, "nedge.edit_users", "host_pools.edit_host_pools"))
|
|
.. [["></i> ]]
|
|
.. i18n(ternary(have_nedge, "nedge.edit_users", "host_pools.edit_host_pools"))
|
|
.. [[</A>
|
|
</tr>]]
|
|
|
|
print(table.concat(output, ''))
|
|
end
|
|
|
|
-- #################################################
|
|
|
|
function printCategoryDropdownButton(by_id, cat_id_or_name, base_url, page_params, count_callback, skip_unknown)
|
|
local function count_all(cat_id, cat_name)
|
|
local cat_protos = interface.getnDPIProtocols(tonumber(cat_id))
|
|
return table.len(cat_protos)
|
|
end
|
|
|
|
cat_id_or_name = cat_id_or_name or ""
|
|
count_callback = count_callback or count_all
|
|
|
|
-- 'Category' button
|
|
print('\'<div class="btn-group float-right"><div class="btn btn-link dropdown-toggle" data-toggle="dropdown">'..
|
|
i18n("category") .. ternary(not isEmptyString(cat_id_or_name), '<span class="fas fa-filter"></span>', '') ..
|
|
'<span class="caret"></span></div> <ul class="dropdown-menu" role="menu" style="min-width: 90px;">')
|
|
|
|
-- 'Category' dropdown menu
|
|
local entries = { {text=i18n("all"), id="", cat_id=""} }
|
|
entries[#entries + 1] = ""
|
|
for cat_name, cat_id in pairsByKeys(interface.getnDPICategories()) do
|
|
local cat_count = count_callback(cat_id, cat_name)
|
|
|
|
if(skip_unknown and (cat_id == "0") and (cat_count > 0)) then
|
|
-- Do not count the Unknown protocol in the Unspecified category
|
|
cat_count = cat_count - 1
|
|
end
|
|
|
|
if cat_count > 0 then
|
|
entries[#entries + 1] = {text=cat_name.." ("..cat_count..")", id=cat_name, cat_id=cat_id}
|
|
end
|
|
end
|
|
|
|
for _, entry in pairs(entries) do
|
|
if entry ~= "" then
|
|
page_params["category"] = ternary(by_id, ternary(entry.cat_id ~= "", "cat_" .. entry.cat_id, ""), entry.id)
|
|
|
|
print('<li' .. ternary(cat_id_or_name == ternary(by_id, entry.cat_id, entry.id), ' class="active"', '') ..
|
|
'><a class="dropdown-item" href="' .. getPageUrl(base_url, page_params) .. '">' .. (entry.icon or "") ..
|
|
entry.text .. '</a></li>')
|
|
else
|
|
print(makeMenuDivider())
|
|
end
|
|
end
|
|
|
|
print('</ul></div>\', ')
|
|
page_params["category"] = cat_id_or_name
|
|
end
|
|
|
|
-- #################################################
|
|
|
|
function getDeviceCommonTimeseries()
|
|
return {
|
|
{schema="mac:arp_rqst_sent_rcvd_rpls", label=i18n("graphs.arp_rqst_sent_rcvd_rpls")},
|
|
}
|
|
end
|
|
|
|
-- #################################################
|