--
-- (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")
-- ########################################################
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 format_utils = require "format_utils"
-- for idx, data in ipairs(series) do
-- for _, s in ipairs(data.series) do
-- if not s.tags.protocol then
-- tprint({step = data.step, num = #s.data, start = format_utils.formatEpoch(data.start), count = s.count, label = s.label})
-- end
-- end
-- end
local max_count = 0
local min_step = math.huge
local ts_common = require("ts_common")
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
serie.count = max_count
for _, serie_data in pairs(serie.series) do
-- The way this function perform the upsampling is partial.
-- Only points are upsampled, times are not adjusted.
-- In addition, the max_count is fixed and this causes series
-- with different lengths to be upsampled differently.
-- For example a 240-points timeseries with lenght 1-day
-- and a 10 points timeseris with length 1-hour would result
-- the the 1-hour timeseries being divided into 240 points, actually
-- ending up in having a much smaller step.
-- TODO: adjust timeseries times.
-- TODO: handle series with different start and end times.
serie_data.data = ts_common.upsampleSerie(serie_data.data, max_count)
-- The new step needs to be adjusted as well. The new step is smaller
-- than the new step. To calculate it, multiply the old step by the fraction
-- of old vs new points.
local new_step = round(serie.step * count / max_count, 0)
serie.step = new_step
serie_data.step = new_step
serie_data.count = max_count
end
end
end
end
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 = ' '..sentLabel
elseif(sent2rcvd > thresholdHigh) then rcvdLabel = ' '..rcvdLabel end
print('
'..sentLabel)
print('
' .. rcvdLabel .. '
')
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('
'..valueLabel)
print('
')
else
print(' ')
end
end
-- ########################################################
function makeProgressBar(percentage)
-- nan check
if percentage ~= percentage then
return ""
end
local perc_int = round(percentage)
return '
'..
round(percentage, 1) ..' %'
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] = [[
]]
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] = [[]]
else
res[#res + 1] = [[
]]
end
if bar.link ~= nil then res[#res + 1] = [[]] end
end
res[#res + 1] = [[
]]
-- The legend
res[#res + 1] = [[
]]
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] = [[]]
if(num > 0) then res[#res + 1] = [[ ]] end
if bar.link ~= nil then res[#res + 1] = [[]] end
res[#res + 1] = [[]]
if bar.link ~= nil then res[#res + 1] = [[]] end
res[#res + 1] = [[ ]] .. bar.title .. " (".. formatter(bar.value) ..")"
num = num + 1
end
res[#res + 1] = [[ - ]] .. i18n("total") .. ": ".. formatter(total) ..""
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 ''
end
function makeMenuHeader(label)
return '
'.. label ..'
'
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] = [[ ]] .. entry.label .. [[]]
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.isEnterpriseM()
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.skip) 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
if(serie.custom_schema == nil) then
serie.custom_schema = getCustomSchemaOptions(k)
end
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[getCategoryLabel(serie.category)] = serie.category
end
for label, category in pairsByKeys(by_category, asc) do
populateGraphMenuEntry(label, 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("" .. i18n("notes").. "
")
for _, note in ipairs(notes_items) do
print("
" ..note .. "
")
end
print("
")
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[[
]]
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 [[
\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('\n')
::continue::
end
print [[
NOTE: Click on the graph to zoom.
]]
local format_as_bps = true
local format_as_bytes = false
local formatter_fctn
local label = data.series[1].label
if label == "load_percentage" then
formatter_fctn = "ffloat"
format_as_bps = false
elseif label == "resident_bytes" then
formatter_fctn = "bytesToSize"
format_as_bytes = true
elseif string.contains(label, "pct") then
formatter_fctn = "fpercent"
format_as_bps = false
format_as_bytes = false
elseif schema == "process:num_alerts" then
formatter_fctn = "falerts"
format_as_bps = false
format_as_bytes = false
elseif label:contains("millis") or label:contains("_ms") then
formatter_fctn = "fmillis"
format_as_bytes = false
format_as_bps = false
elseif string.contains(label, "packets") or string.contains(label, "flows") or label:starts("num_") or label:contains("alerts") then
formatter_fctn = "fint"
format_as_bytes = false
format_as_bps = false
else
formatter_fctn = "fbits"
end
print [[
]]
print('
Time
Value
\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 0
local maxval_time = stats.max_val_idx and (data.start + data.step * stats.max_val_idx) or 0
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] or 0)
end
if format_as_bytes then
if(minval_time > 0) then print('
\n')
if top_talkers_utils.areTopEnabled(ifid) then
print('
Minute Interface Top Talkers
\n')
end
print [[
]]
print[[
]]
if show_historical_tabs then
local host = tags.host -- can be nil
local l7proto = tags.protocol or ""
local k2info = hostkey2hostinfo(host)
print('
')
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('
')
end
print[[
]]
else
print("
No data found
")
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] = [[
") 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] = [[
") 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] = ''
end
end
return table.concat(output, '')
end
-- #################################################
function printPoolChangeDropdown(ifId, pool_id, have_nedge)
local output = {}
output[#output + 1] = [[
]]
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('\'
')
-- '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('