mirror of
https://github.com/ntop/ntopng.git
synced 2026-04-30 07:59:35 +00:00
Plugins are a convenient way to group together related lua scripts. Their primary use case is to group user scripts and their alert/status definition. The builtin ntopng user scripts and definitions are now packed into plugins directories. In future, we will support loading of user created plugins. Plugins are loaded at startup into some runtime directories and then used. Other changes provided by this commit include: - Add sample flow logger plugin - Initial support for system user scripts - Rename edge to threshold - Migrate system probes to user scripts/plugins - Migrate scripts to more explicit alerts_api.checkThresholdAlert api
928 lines
32 KiB
Lua
928 lines
32 KiB
Lua
--
|
|
-- (C) 2019 - ntop.org
|
|
--
|
|
|
|
-- User scripts provide a scriptable way to interact with the ntopng
|
|
-- core. Users can provide their own modules to trigger custom alerts,
|
|
-- export data, or perform periodic tasks.
|
|
|
|
local os_utils = require("os_utils")
|
|
local json = require("dkjson")
|
|
local plugins_utils = require("plugins_utils")
|
|
|
|
local user_scripts = {}
|
|
|
|
-- ##############################################
|
|
|
|
user_scripts.field_units = {
|
|
seconds = "field_units.seconds",
|
|
bytes = "field_units.bytes",
|
|
flows = "field_units.flows",
|
|
packets = "field_units.packets",
|
|
mbits = "field_units.mbits",
|
|
hosts = "field_units.hosts",
|
|
syn_sec = "field_units.syn_sec",
|
|
flow_sec = "field_units.flow_sec",
|
|
percentage = "field_units.percentage",
|
|
syn_min = "field_units.syn_min",
|
|
}
|
|
|
|
local CALLBACKS_DIR = plugins_utils.PLUGINS_RUNTIME_PATH .. "/callbacks"
|
|
local NON_TRAFFIC_ELEMENT_CONF_KEY = "all"
|
|
local NON_TRAFFIC_ELEMENT_ENTITY = "no_entity"
|
|
|
|
-- Hook points for flow/periodic modules
|
|
-- NOTE: keep in sync with the Documentation
|
|
user_scripts.script_types = {
|
|
flow = {
|
|
parent_dir = "interface",
|
|
hooks = {"protocolDetected", "statusChanged", "flowEnd", "periodicUpdate"},
|
|
}, traffic_element = {
|
|
parent_dir = "interface",
|
|
hooks = {"min", "5mins", "hour", "day"},
|
|
}, snmp_device = {
|
|
parent_dir = "system",
|
|
hooks = {"snmpDeviceInterface"},
|
|
}, system = {
|
|
parent_dir = "system",
|
|
hooks = {"min", "5mins", "hour", "day"},
|
|
}, syslog = {
|
|
parent_dir = "syslog",
|
|
hooks = {"handleEvent"},
|
|
}
|
|
}
|
|
|
|
-- ##############################################
|
|
|
|
-- Table to keep per-subdir then per-module then per-hook benchmarks
|
|
--
|
|
-- The structure is the following
|
|
--
|
|
-- table
|
|
-- flow table
|
|
-- flow.mud table
|
|
-- flow.mud.protocolDetected table
|
|
-- flow.mud.protocolDetected.tot_elapsed number 0.00031600000000021
|
|
-- flow.mud.protocolDetected.tot_num_calls number 4
|
|
-- flow.score table
|
|
-- flow.score.protocolDetected table
|
|
-- flow.score.protocolDetected.tot_elapsed number 0.00013700000000005
|
|
-- flow.score.protocolDetected.tot_num_calls number 4
|
|
-- flow.score.statusChanged table
|
|
-- flow.score.statusChanged.tot_elapsed number 0
|
|
-- flow.score.statusChanged.tot_num_calls number 0
|
|
local benchmarks = {}
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getSubdirectoryPath(script_type, subdir, is_pro)
|
|
local prefix = CALLBACKS_DIR
|
|
local path
|
|
|
|
if not isEmptyString(subdir) and subdir ~= "." then
|
|
path = string.format("%s/%s/%s", prefix, script_type.parent_dir, subdir)
|
|
else
|
|
path = string.format("%s/%s", prefix, script_type.parent_dir)
|
|
end
|
|
|
|
return os_utils.fixPath(path)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Get the default configuration for the given user script
|
|
-- and granularity.
|
|
-- @param user_script a user_script returned by user_scripts.load
|
|
-- @param granularity_str the target granularity
|
|
-- @return a table with the default configuration
|
|
function user_scripts.getDefaultConfig(user_script, granularity_str)
|
|
local conf = {script_conf = {}, enabled = user_script.default_enabled}
|
|
|
|
if((user_script.default_values ~= nil) and (user_script.default_values[granularity_str] ~= nil)) then
|
|
-- granularity specific default
|
|
conf.script_conf = user_script.default_values[granularity_str] or {}
|
|
else
|
|
conf.script_conf = user_script.default_value or {}
|
|
end
|
|
|
|
return(conf)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Wrap any hook function to compute its execution time which is then added
|
|
-- to the benchmarks table.
|
|
--
|
|
-- @param subdir the modules subdir
|
|
-- @param mod_k the key of the user script
|
|
-- @param hook the name of the hook in the user script
|
|
-- @param hook_fn the hook function in the user script
|
|
--
|
|
-- @return function(...) wrapper ready to be called for the execution of hook_fn
|
|
local function benchmark_hook_fn(subdir, mod_k, hook, hook_fn)
|
|
return function(...)
|
|
local start = ntop.getticks()
|
|
local result = {hook_fn(...)}
|
|
local finish = ntop.getticks()
|
|
local elapsed = finish - start
|
|
|
|
-- Update benchmark results by addin a function call and the elapsed time of this call
|
|
benchmarks[subdir][mod_k][hook]["tot_num_calls"] = benchmarks[subdir][mod_k][hook]["tot_num_calls"] + 1
|
|
benchmarks[subdir][mod_k][hook]["tot_elapsed"] = benchmarks[subdir][mod_k][hook]["tot_elapsed"] + elapsed
|
|
|
|
-- traceError(TRACE_NORMAL,TRACE_CONSOLE, string.format("[%s][elapsed: %.2f][tot_elapsed: %.2f][tot_num_calls: %u]",
|
|
-- hook, elapsed,
|
|
-- benchmarks[subdir][mod_k][hook]["tot_elapsed"],
|
|
-- benchmarks[subdir][mod_k][hook]["tot_num_calls"]))
|
|
|
|
return table.unpack(result)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Initializes benchmark facilities for any hook function
|
|
--
|
|
-- @param subdir the modules subdir
|
|
-- @param mod_k the key of the user script
|
|
-- @param hook the name of the hook in the user script
|
|
-- @param hook_fn the hook function in the user script
|
|
--
|
|
-- @return function(...) wrapper ready to be called for the execution of hook_fn
|
|
local function benchmark_init(subdir, mod_k, hook, hook_fn)
|
|
-- NOTE: 5min/hour/day are not monitored. They would collide in the user_scripts_benchmarks_key.
|
|
if((hook ~= "5min") and (hook ~= "hour") and (hook ~= "day")) then
|
|
-- Prepare the benchmark table fo the hook_fn which is being benchmarked
|
|
if not benchmarks[subdir] then
|
|
benchmarks[subdir] = {}
|
|
end
|
|
|
|
if not benchmarks[subdir][mod_k] then
|
|
benchmarks[subdir][mod_k] = {}
|
|
end
|
|
|
|
if not benchmarks[subdir][mod_k][hook] then
|
|
benchmarks[subdir][mod_k][hook] = {tot_num_calls = 0, tot_elapsed = 0}
|
|
end
|
|
|
|
-- Finally prepare and return the hook_fn wrapped with benchmark facilities
|
|
return benchmark_hook_fn(subdir, mod_k, hook, hook_fn)
|
|
else
|
|
return(hook_fn)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
--~ schema_prefix: "flow_user_script" or "elem_user_script"
|
|
function user_scripts.ts_dump(when, ifid, verbose, schema_prefix, all_scripts)
|
|
local ts_utils = require("ts_utils_core")
|
|
|
|
for subdir, script_type in pairs(all_scripts) do
|
|
local rv = user_scripts.getAggregatedStats(ifid, script_type, subdir)
|
|
local total = {tot_elapsed = 0, tot_num_calls = 0}
|
|
|
|
for modkey, stats in pairs(rv) do
|
|
ts_utils.append(schema_prefix .. ":duration", {ifid = ifid, user_script = modkey, subdir = subdir, num_ms = stats.tot_elapsed * 1000}, when, verbose)
|
|
ts_utils.append(schema_prefix .. ":num_calls", {ifid = ifid, user_script = modkey, subdir = subdir, num_calls = stats.tot_num_calls}, when, verbose)
|
|
|
|
total.tot_elapsed = total.tot_elapsed + stats.tot_elapsed
|
|
total.tot_num_calls = total.tot_num_calls + stats.tot_num_calls
|
|
end
|
|
|
|
ts_utils.append(schema_prefix .. ":total_stats", {ifid = ifid, subdir = subdir, num_ms = total.tot_elapsed * 1000, num_calls = total.tot_num_calls}, when, verbose)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function user_scripts_benchmarks_key(ifid, subdir)
|
|
return string.format("ntopng.cache.ifid_%d.user_scripts_benchmarks.subdir_%s", ifid, subdir)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Returns the benchmark stats, aggregating them by module
|
|
function user_scripts.getAggregatedStats(ifid, script_type, subdir)
|
|
local bencmark = ntop.getCache(user_scripts_benchmarks_key(ifid, subdir))
|
|
local rv = {}
|
|
|
|
if(not isEmptyString(bencmark)) then
|
|
bencmark = json.decode(bencmark)
|
|
|
|
if(bencmark ~= nil) then
|
|
for scriptk, hooks in pairs(bencmark) do
|
|
local aggr_val = {tot_num_calls = 0, tot_elapsed = 0}
|
|
|
|
for _, hook_benchmark in pairs(hooks) do
|
|
aggr_val.tot_elapsed = aggr_val.tot_elapsed + hook_benchmark.tot_elapsed
|
|
aggr_val.tot_num_calls = hook_benchmark.tot_num_calls + aggr_val.tot_num_calls
|
|
end
|
|
|
|
if(aggr_val.tot_num_calls > 0) then
|
|
rv[scriptk] = aggr_val
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Save benchmarks results and possibly print them to stdout
|
|
--
|
|
-- @param to_stdout dump results also to stdout
|
|
function user_scripts.benchmark_dump(ifid, to_stdout)
|
|
-- Convert ticks to seconds
|
|
for subdir, modules in pairs(benchmarks) do
|
|
local rv = {}
|
|
|
|
for mod_k, hooks in pairs(modules) do
|
|
for hook, hook_benchmark in pairs(hooks) do
|
|
if hook_benchmark["tot_num_calls"] > 0 then
|
|
hook_benchmark["tot_elapsed"] = hook_benchmark["tot_elapsed"] / ntop.gettickspersec()
|
|
|
|
rv[mod_k] = rv[mod_k] or {}
|
|
rv[mod_k][hook] = hook_benchmark
|
|
|
|
if to_stdout then
|
|
traceError(TRACE_NORMAL,TRACE_CONSOLE,
|
|
string.format("[%s] %s() [script: %s][elapsed: %.4f][num: %u][speed: %.4f]\n",
|
|
subdir, hook, mod_k, hook_benchmark["tot_elapsed"], hook_benchmark["tot_num_calls"],
|
|
hook_benchmark["tot_elapsed"] / hook_benchmark["tot_num_calls"]))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
ntop.setCache(user_scripts_benchmarks_key(ifid, subdir), json.encode(rv), 3600 --[[ 1 hour --]])
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function getScriptsDirectories(script_type, subdir)
|
|
local check_dirs = {
|
|
user_scripts.getSubdirectoryPath(script_type, subdir),
|
|
}
|
|
|
|
return(check_dirs)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Lists available user scripts.
|
|
-- @params script_type one of user_scripts.script_types
|
|
-- @params subdir the modules subdir
|
|
-- @return a list of available module names
|
|
function user_scripts.listScripts(script_type, subdir)
|
|
local check_dirs = getScriptsDirectories(script_type, subdir)
|
|
local rv = {}
|
|
|
|
for _, checks_dir in pairs(check_dirs) do
|
|
for fname in pairs(ntop.readdir(checks_dir)) do
|
|
if string.ends(fname, ".lua") then
|
|
local mod_fname = string.sub(fname, 1, string.len(fname) - 4)
|
|
rv[#rv + 1] = mod_fname
|
|
end
|
|
end
|
|
end
|
|
|
|
return rv
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getLastBenchmark(ifid, subdir)
|
|
local scripts_benchmarks = ntop.getCache(user_scripts_benchmarks_key(ifid, subdir))
|
|
|
|
if(not isEmptyString(scripts_benchmarks)) then
|
|
scripts_benchmarks = json.decode(scripts_benchmarks)
|
|
else
|
|
scripts_benchmarks = nil
|
|
end
|
|
|
|
return(scripts_benchmarks)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function getConfigurationKey(subdir)
|
|
-- NOTE: strings needed by user_scripts.deleteConfigurations
|
|
-- NOTE: The configuration must not be saved under a specific ifid, since we
|
|
-- allow global interfaces configurations
|
|
return(string.format("ntopng.prefs.user_scripts.conf.%s", subdir))
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Get the user scripts configuration
|
|
-- @param subdir: the subdir
|
|
-- @return a table
|
|
-- {[hook] = {entity_value -> {enabled=true, script_conf = {a = 1}, }, ..., default -> {enabled=false, script_conf = {}, }}, ...}
|
|
-- @note debug with: redis-cli get ntopng.prefs.user_scripts.conf.interface | python -m json.tool
|
|
local function loadConfiguration(subdir)
|
|
local key = getConfigurationKey(subdir)
|
|
local value = ntop.getPref(key)
|
|
|
|
if(not isEmptyString(value)) then
|
|
value = json.decode(value) or {}
|
|
else
|
|
value = {}
|
|
end
|
|
|
|
return(value)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Save the user scripts configuration.
|
|
-- @param subdir: the subdir
|
|
-- @param config: the configuration to save
|
|
local function saveConfiguration(subdir, config)
|
|
local key = getConfigurationKey(subdir)
|
|
|
|
if(table.empty(config)) then
|
|
ntop.delCache(key)
|
|
else
|
|
local value = json.encode(config)
|
|
ntop.setPref(key, value)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.deleteConfigurations()
|
|
deleteCachePattern(getConfigurationKey("*"))
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- This needs to be called whenever the available_modules.conf changes
|
|
-- It updates the single scripts config
|
|
local function reload_scripts_config(available_modules)
|
|
local scripts_conf = available_modules.conf
|
|
|
|
for _, script in pairs(available_modules.modules) do
|
|
script.conf = scripts_conf[script.key] or {}
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function delete_script_conf(scripts_conf, key, hook, conf_key)
|
|
if(scripts_conf[key] and scripts_conf[key][hook]) then
|
|
scripts_conf[key][hook][conf_key] = nil
|
|
|
|
-- Cleanup empty tables
|
|
if table.empty(scripts_conf[key][hook]) then
|
|
scripts_conf[key][hook] = nil
|
|
|
|
if table.empty(scripts_conf[key]) then
|
|
scripts_conf[key] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Load the user scripts.
|
|
-- @param ifid the interface ID
|
|
-- @param script_type one of user_scripts.script_types
|
|
-- @param subdir the modules subdir. *NOTE* this must be unique as it is used as a key.
|
|
-- @param options an optional table with the following supported options:
|
|
-- - hook_filter: if non nil, only load the user scripts for the specified hook
|
|
-- - do_benchmark: if true, computes benchmarks for every hook
|
|
-- - return_all: if true, returns all the scripts, even those with filters not matching the current configuration
|
|
-- NOTE: this can only be applied if the script type has the "has_no_entity" flag set.
|
|
-- - scripts_filter: a filter function(user_script) -> true, false. false will cause the script to be skipped.
|
|
-- @return {modules = key->user_script, hooks = user_script->function}
|
|
function user_scripts.load(ifid, script_type, subdir, options)
|
|
local rv = {modules = {}, hooks = {}, conf = {}}
|
|
local is_nedge = ntop.isnEdge()
|
|
local is_windows = ntop.isWindows()
|
|
local alerts_disabled = (not areAlertsEnabled())
|
|
local old_ifid = interface.getId()
|
|
options = options or {}
|
|
|
|
-- Load additional schemas
|
|
plugins_utils.loadSchemas(options.hook_filter)
|
|
|
|
local hook_filter = options.hook_filter
|
|
local do_benchmark = options.do_benchmark
|
|
local return_all = options.return_all
|
|
local scripts_filter = options.scripts_filter
|
|
|
|
if(old_ifid ~= ifid) then
|
|
interface.select(ifid) -- required for interface.isPacketInterface() below
|
|
end
|
|
|
|
for _, hook in pairs(script_type.hooks) do
|
|
rv.hooks[hook] = {}
|
|
end
|
|
|
|
local check_dirs = getScriptsDirectories(script_type, subdir)
|
|
rv.conf = loadConfiguration(subdir)
|
|
|
|
for _, checks_dir in pairs(check_dirs) do
|
|
package.path = checks_dir .. "/?.lua;" .. package.path
|
|
|
|
local is_alert_path = string.ends(checks_dir, "alerts")
|
|
|
|
for fname in pairs(ntop.readdir(checks_dir)) do
|
|
if string.ends(fname, ".lua") then
|
|
local mod_fname = string.sub(fname, 1, string.len(fname) - 4)
|
|
local user_script = require(mod_fname)
|
|
local setup_ok = true
|
|
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Loading user script '%s'", mod_fname))
|
|
|
|
if(type(user_script) ~= "table") then
|
|
traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Loading '%s' failed", checks_dir.."/"..fname))
|
|
goto next_module
|
|
end
|
|
|
|
-- Key is an alias for the module name
|
|
user_script.key = mod_fname
|
|
|
|
if(rv.modules[user_script.key]) then
|
|
traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Skipping duplicate module '%s'", user_script.key))
|
|
goto next_module
|
|
end
|
|
|
|
if((not return_all) and user_script.packet_interface_only and (not interface.isPacketInterface())) then
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping module '%s' for non packet interface", user_script.key))
|
|
goto next_module
|
|
end
|
|
|
|
if((not return_all) and ((user_script.nedge_exclude and is_nedge) or (user_script.nedge_only and (not is_nedge)))) then
|
|
goto next_module
|
|
end
|
|
|
|
if((not return_all) and (user_script.windows_exclude and is_windows)) then
|
|
goto next_module
|
|
end
|
|
|
|
if(table.empty(user_script.hooks)) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("No 'hooks' defined in user script '%s', skipping", user_script.key))
|
|
goto next_module
|
|
end
|
|
|
|
if(user_script.l7_proto ~= nil) then
|
|
user_script.l7_proto_id = interface.getnDPIProtoId(user_script.l7_proto)
|
|
|
|
if(user_script.l7_proto_id == -1) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Unknown L7 protocol filter '%s' in user script '%s', skipping", user_script.l7_proto, user_script.key))
|
|
goto next_module
|
|
end
|
|
end
|
|
|
|
-- Augument with additional attributes
|
|
user_script.is_alert = is_alert_path
|
|
user_script.path = os_utils.fixPath(checks_dir .. "/" .. fname)
|
|
user_script.default_enabled = ternary(user_script.default_enabled == false, false, true --[[ a nil value means enabled ]])
|
|
|
|
if((not return_all) and alerts_disabled and user_script.is_alert) then
|
|
goto next_module
|
|
end
|
|
|
|
if(hook_filter ~= nil) then
|
|
-- Only return modules which should be called for the specified hook
|
|
if((user_script.hooks[hook_filter] == nil) and (user_script.hooks["all"] == nil)) then
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping module '%s' for hook '%s'", user_script.key, hook_filter))
|
|
goto next_module
|
|
end
|
|
end
|
|
|
|
-- Load the configuration
|
|
user_script.conf = rv.conf[user_script.key] or {}
|
|
|
|
-- TODO remove after gui migration
|
|
if(user_script.gui and (user_script.gui.input_builder == nil)) then
|
|
user_script.gui.input_builder = user_scripts.checkbox_input_builder
|
|
end
|
|
if(user_script.gui and user_script.gui.post_handler == nil) then
|
|
user_script.gui.post_handler = user_scripts.getDefaultPostHandler(user_script.gui.input_builder) or user_scripts.checkbox_post_handler
|
|
end
|
|
-- end TODO
|
|
|
|
if(user_script.gui and user_script.gui.input_builder and (not user_script.gui.post_handler)) then
|
|
-- Try to use a default post handler
|
|
user_script.gui.post_handler = user_scripts.getDefaultPostHandler(user_script.gui.input_builder)
|
|
|
|
if(user_script.gui.post_handler == nil) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Module '%s' is missing the gui.post_handler", user_script.key))
|
|
end
|
|
end
|
|
|
|
if(scripts_filter ~= nil) then
|
|
local script_ok = scripts_filter(user_script)
|
|
|
|
if(not script_ok) then
|
|
goto next_module
|
|
end
|
|
end
|
|
|
|
-- If a setup function is available, call it
|
|
if(user_script.setup ~= nil) then
|
|
setup_ok = user_script.setup()
|
|
end
|
|
|
|
if((not return_all) and (not setup_ok)) then
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping module '%s' as setup() returned %s", user_script.key, setup_ok))
|
|
goto next_module
|
|
end
|
|
|
|
-- Checks passed, now load the script information
|
|
|
|
-- Populate hooks fast lookup table
|
|
for hook, hook_fn in pairs(user_script.hooks) do
|
|
-- load previously computed benchmarks (if any)
|
|
-- benchmarks are loaded even if their computation is disabled with a do_benchmark ~= true
|
|
if(hook == "all") then
|
|
-- Register for all the hooks
|
|
for _, hook in pairs(script_type.hooks) do
|
|
if do_benchmark then
|
|
rv.hooks[hook][user_script.key] = benchmark_init(subdir, user_script.key, hook, hook_fn)
|
|
else
|
|
rv.hooks[hook][user_script.key] = hook_fn
|
|
end
|
|
end
|
|
|
|
-- no more hooks allowed
|
|
break
|
|
elseif(rv.hooks[hook] == nil) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Unknown hook '%s' in module '%s'", hook, user_script.key))
|
|
else
|
|
|
|
if do_benchmark then
|
|
rv.hooks[hook][user_script.key] = benchmark_init(subdir, user_script.key, hook, hook_fn)
|
|
else
|
|
rv.hooks[hook][user_script.key] = hook_fn
|
|
end
|
|
end
|
|
end
|
|
|
|
if(rv.hooks["periodicUpdate"] ~= nil) then
|
|
-- Set the update frequency
|
|
local default_update_freq = 120 -- Default: every 2 minutes
|
|
|
|
if(user_script.periodic_update_seconds ~= nil) then
|
|
if((user_script.periodic_update_seconds % 30) ~= 0) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format(
|
|
"Update_periodicity '%s' is not multiple of 30 in '%s', using default (%u)",
|
|
user_script.periodic_update_seconds, user_script.key, default_update_freq))
|
|
user_script.periodic_update_seconds = default_update_freq
|
|
end
|
|
else
|
|
user_script.periodic_update_seconds = default_update_freq
|
|
end
|
|
|
|
user_script.periodic_update_divisor = math.floor(user_script.periodic_update_seconds / 30)
|
|
end
|
|
|
|
rv.modules[user_script.key] = user_script
|
|
end
|
|
|
|
::next_module::
|
|
end
|
|
end
|
|
|
|
if(old_ifid ~= ifid) then
|
|
interface.select(old_ifid)
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Convenient method to only load a specific script
|
|
function user_scripts.loadModule(ifid, script_type, subdir, mod_fname)
|
|
local check_dirs = getScriptsDirectories(script_type, subdir)
|
|
|
|
for _, checks_dir in pairs(check_dirs) do
|
|
local full_path = os_utils.fixPath(checks_dir .. "/" .. mod_fname .. ".lua")
|
|
|
|
if ntop.exists(full_path) then
|
|
return(assert(loadfile(full_path))())
|
|
end
|
|
end
|
|
|
|
return(nil)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Get the configuration to use for a specific entity
|
|
-- @param user_script the user script, loaded with user_scripts.load
|
|
-- @param (optional) hook the hook function
|
|
-- @param (optional) entity_value the entity value
|
|
-- @param (optional) is_remote_host, for hosts only, indicates if the entity is a remote host
|
|
-- @return the script configuration as a table
|
|
function user_scripts.getConfiguration(user_script, hook, entity_value, is_remote_host)
|
|
local rv = nil
|
|
hook = hook or NON_TRAFFIC_ELEMENT_CONF_KEY
|
|
entity_value = entity_value or NON_TRAFFIC_ELEMENT_ENTITY
|
|
local conf = user_script.conf[hook]
|
|
|
|
-- A configuration may not exist for the given hook
|
|
if(conf ~= nil) then
|
|
-- Search for this specific entity config
|
|
rv = conf[entity_value]
|
|
end
|
|
|
|
if(rv == nil) then
|
|
-- Search for a global/default configuration
|
|
rv = user_scripts.getGlobalConfiguration(user_script, hook, is_remote_host)
|
|
end
|
|
|
|
if(rv.script_conf == nil) then
|
|
-- Use the default
|
|
rv.script_conf = user_script.default_value or {}
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function get_global_conf_key(is_remote_host)
|
|
return(ternary(is_remote_host, "global_remote", "global"))
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Get the global configuration to use for a all the entities of this user_script
|
|
-- @param user_script the user script, loaded with user_scripts.load
|
|
-- @param hook the hook function
|
|
-- @param is_remote_host, for hosts only, indicates if the entity is a remote host
|
|
-- @return the script configuration as a table
|
|
function user_scripts.getGlobalConfiguration(user_script, hook, is_remote_host)
|
|
local conf = user_script.conf[hook]
|
|
local rv = nil
|
|
|
|
if(conf ~= nil) then
|
|
rv = conf[get_global_conf_key(is_remote_host)]
|
|
end
|
|
|
|
if(rv == nil) then
|
|
-- No Specific/Global configuration found, try defaults
|
|
rv = user_scripts.getDefaultConfig(user_script, hook)
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Delete the configuration of a specific element (e.g. a specific host)
|
|
function user_scripts.deleteSpecificConfiguration(subdir, available_modules, hook, entity_value)
|
|
hook = hook or NON_TRAFFIC_ELEMENT_CONF_KEY
|
|
entity_value = entity_value or NON_TRAFFIC_ELEMENT_ENTITY
|
|
|
|
local scripts_conf = available_modules.conf
|
|
|
|
for _, script in pairs(available_modules.modules) do
|
|
delete_script_conf(scripts_conf, script.key, hook, entity_value)
|
|
end
|
|
|
|
reload_scripts_config(available_modules)
|
|
saveConfiguration(subdir, scripts_conf)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Delete the configuration for all the elements in subdir (e.g. all the hosts)
|
|
function user_scripts.deleteGlobalConfiguration(subdir, available_modules, hook, remote_host)
|
|
return(user_scripts.deleteSpecificConfiguration(subdir, available_modules, hook, get_global_conf_key(remote_host)))
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.runPeriodicScripts(granularity)
|
|
if(granularity == "min") then
|
|
interface.checkInterfaceAlertsMin()
|
|
interface.checkHostsAlertsMin()
|
|
interface.checkNetworksAlertsMin()
|
|
elseif(granularity == "5mins") then
|
|
interface.checkInterfaceAlerts5Min()
|
|
interface.checkHostsAlerts5Min()
|
|
interface.checkNetworksAlerts5Min()
|
|
elseif(granularity == "hour") then
|
|
interface.checkInterfaceAlertsHour()
|
|
interface.checkHostsAlertsHour()
|
|
interface.checkNetworksAlertsHour()
|
|
elseif(granularity == "day") then
|
|
interface.checkInterfaceAlertsDay()
|
|
interface.checkHostsAlertsDay()
|
|
interface.checkNetworksAlertsDay()
|
|
else
|
|
traceError(TRACE_ERROR, TRACE_CONSOLE, "Unknown granularity " .. granularity)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.checkbox_input_builder(gui_conf, submit_field, active)
|
|
local on_value = "on"
|
|
local off_value = "off"
|
|
local value
|
|
local on_color = "success"
|
|
local off_color = "danger"
|
|
submit_field = "enabled_" .. submit_field
|
|
|
|
local on_active
|
|
local off_active
|
|
|
|
if active then
|
|
|
|
value = on_value
|
|
on_active = "btn-"..on_color.." active"
|
|
off_active = "btn-secondary"
|
|
else
|
|
value = off_value
|
|
on_active = "btn-secondary"
|
|
off_active = "btn-"..off_color.." active"
|
|
end
|
|
|
|
return [[
|
|
<div class="btn-group btn-toggle">
|
|
<button type="button" onclick="]]..submit_field..[[_on_fn()" id="]]..submit_field..[[_on_id" class="btn btn-sm ]]..on_active..[[">On</button>
|
|
<button type="button" onclick="]]..submit_field..[[_off_fn()" id="]]..submit_field..[[_off_id" class="btn btn-sm ]]..off_active..[[">Off</button>
|
|
</div>
|
|
<input type=hidden id="]]..submit_field..[[_input" name="]]..submit_field..[[" value="]]..value..[["/>
|
|
<script>
|
|
|
|
|
|
function ]]..submit_field..[[_on_fn() {
|
|
var class_on = document.getElementById("]]..submit_field..[[_on_id");
|
|
var class_off = document.getElementById("]]..submit_field..[[_off_id");
|
|
class_on.removeAttribute("class");
|
|
class_off.removeAttribute("class");
|
|
class_on.setAttribute("class", "btn btn-sm btn-]]..on_color..[[ active");
|
|
class_off.setAttribute("class", "btn btn-sm btn-secondary");
|
|
$("#]]..submit_field..[[_input").val("]]..on_value..[[").trigger('change');
|
|
}
|
|
|
|
function ]]..submit_field..[[_off_fn() {
|
|
var class_on = document.getElementById("]]..submit_field..[[_on_id");
|
|
var class_off = document.getElementById("]]..submit_field..[[_off_id");
|
|
class_on.removeAttribute("class");
|
|
class_off.removeAttribute("class");
|
|
class_on.setAttribute("class", "btn btn-sm btn-secondary");
|
|
class_off.setAttribute("class", "btn btn-sm btn-]]..off_color..[[ active");
|
|
$("#]]..submit_field..[[_input").val("]]..off_value..[[").trigger('change');
|
|
}
|
|
</script>
|
|
]]
|
|
end
|
|
|
|
function user_scripts.checkbox_post_handler(submit_field)
|
|
-- TODO remove after implementing the new gui
|
|
return(nil)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.threshold_cross_input_builder(gui_conf, input_id, value)
|
|
value = value or {}
|
|
local gt_selected = ternary((value.operator or gui_conf.field_operator) == "gt", ' selected="selected"', '')
|
|
local lt_selected = ternary((value.operator or gui_conf.field_operator) == "lt", ' selected="selected"', '')
|
|
local input_op = "op_" .. input_id
|
|
local input_val = "value_" .. input_id
|
|
|
|
return(string.format([[<select name="%s">
|
|
<option value="gt"%s ]] .. (ternary(gui_conf.field_operator == "lt", "hidden", "")) .. [[>></option>
|
|
<option value="lt"%s ]] .. (ternary(gui_conf.field_operator == "gt", "hidden", "")) .. [[><</option>
|
|
</select> <input type="number" class="text-right form-control" min="%s" max="%s" step="%s" style="display:inline; width:12em;" name="%s" value="%s"/> <span>%s</span>]],
|
|
input_op, gt_selected, lt_selected,
|
|
gui_conf.field_min or "0", gui_conf.field_max or "", gui_conf.field_step or "1",
|
|
input_val, value.threshold, i18n(gui_conf.i18n_field_unit))
|
|
)
|
|
end
|
|
|
|
function user_scripts.threshold_cross_post_handler(input_id)
|
|
local input_op = _POST["op_" .. input_id]
|
|
local input_val = tonumber(_POST["value_" .. input_id])
|
|
|
|
if(input_val ~= nil) then
|
|
return {
|
|
operator = input_op,
|
|
threshold = input_val,
|
|
}
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- For built-in input_builders, return the _POST handler to use
|
|
local input_builder_to_post_handler = {
|
|
[user_scripts.threshold_cross_input_builder] = user_scripts.threshold_cross_post_handler,
|
|
}
|
|
|
|
function user_scripts.getDefaultPostHandler(input_builder)
|
|
return(input_builder_to_post_handler[input_builder])
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Teardown function, to be called at the end of the VM
|
|
function user_scripts.teardown(available_modules, do_benchmark, do_print_benchmark)
|
|
for _, script in pairs(available_modules.modules) do
|
|
if script.teardown then
|
|
script.teardown()
|
|
end
|
|
end
|
|
|
|
if do_benchmark then
|
|
local ifid = interface.getId()
|
|
user_scripts.benchmark_dump(ifid, do_print_benchmark)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.handlePOST(subdir, available_modules, hook, entity_value, remote_host)
|
|
if(table.empty(_POST)) then
|
|
return
|
|
end
|
|
|
|
hook = hook or NON_TRAFFIC_ELEMENT_CONF_KEY
|
|
entity_value = entity_value or NON_TRAFFIC_ELEMENT_ENTITY
|
|
|
|
local scripts_conf = available_modules.conf
|
|
|
|
for _, user_script in pairs(available_modules.modules) do
|
|
-- There are 3 different configurations:
|
|
-- - specific_config: the configuration specific of an host/interface/network
|
|
-- - global_config: the configuration specific for all the (local/remote) hosts, interfaces, networks
|
|
-- - default_config: the default configuration, specified by the user script
|
|
-- They follow the follwing priorities:
|
|
-- [lower] specific_config > global_config > default [upper]
|
|
--
|
|
-- Moreover:
|
|
-- - specific_config is only set if it differs from the global_config
|
|
-- - global_config is only set if it differs from the default_config
|
|
--
|
|
|
|
-- This is used to represent the previous config in order of priority in order
|
|
-- to determine if the current config differs from its default.
|
|
local upper_config = user_scripts.getDefaultConfig(user_script, hook)
|
|
|
|
-- NOTE: we must process the global_config before the specific_config
|
|
for _, prefix in ipairs({"global_", ""}) do
|
|
local k = prefix .. user_script.key
|
|
local is_global = (prefix == "global_")
|
|
local enabled_k = "enabled_" .. k
|
|
local is_enabled = _POST[enabled_k]
|
|
local conf_key = ternary(is_global, get_global_conf_key(remote_host), entity_value)
|
|
local script_conf = nil
|
|
|
|
if(user_script.gui and (user_script.gui.post_handler ~= nil)) then
|
|
script_conf = user_script.gui.post_handler(k)
|
|
end
|
|
|
|
if(is_enabled == nil) then
|
|
-- TODO remove this after changing the gui to support a separate on/off field
|
|
-- For backward compatibility, an empty configuration means that the script is disabled
|
|
|
|
if(user_script.gui and (user_script.gui.post_handler ~= nil) and (subdir ~= "flow")) then
|
|
is_enabled = not table.empty(script_conf)
|
|
else
|
|
is_enabled = user_script.default_enabled
|
|
end
|
|
else
|
|
is_enabled = (is_enabled == "on")
|
|
end
|
|
|
|
local cur_config = {
|
|
enabled = is_enabled,
|
|
script_conf = script_conf,
|
|
}
|
|
|
|
if(not table.compare(upper_config, cur_config)) then
|
|
-- Configuration differs
|
|
scripts_conf[user_script.key] = scripts_conf[user_script.key] or {}
|
|
scripts_conf[user_script.key][hook] = scripts_conf[user_script.key][hook] or {}
|
|
scripts_conf[user_script.key][hook][conf_key] = cur_config
|
|
else
|
|
-- Use the default
|
|
delete_script_conf(scripts_conf, user_script.key, hook, conf_key)
|
|
end
|
|
|
|
-- Needed for specific_config vs global_config comparison
|
|
upper_config = cur_config
|
|
end
|
|
end
|
|
|
|
reload_scripts_config(available_modules)
|
|
saveConfiguration(subdir, scripts_conf)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
return(user_scripts)
|