ntopng/scripts/lua/modules/user_scripts.lua
emanuele-f 15c013922d Improve plugins reload to avoid transient errors
A "shadow directory" is now populated when the reload occurs and then swapped as the active directory.
This avoids breaking the directory structure or changing files when other threads are possibly working
on them.

Fixes #3595
2020-03-26 14:21:11 +01:00

1298 lines
39 KiB
Lua

--
-- (C) 2019-20 - 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 dirs = ntop.getDirs()
local info = ntop.getInfo()
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 NON_TRAFFIC_ELEMENT_CONF_KEY = "all"
local NON_TRAFFIC_ELEMENT_ENTITY = "no_entity"
local ALL_HOOKS_CONFIG_KEY = "all"
local CONFIGSETS_KEY = "ntopng.prefs.user_scripts.configsets_v2"
user_scripts.DEFAULT_CONFIGSET_ID = 0
-- NOTE: the subdir id must be unique
-- target_type: when used with configsets, specifies the allowed target.
-- - cidr: IPv4/IPv6 address or CIDR (e.g. 192.168.0.0/16, 1.2.3.4)
-- - interface: a network interface name (e.g. eth0)
-- - network: a local network CIDR (e.g. 192.168.0.0/24)
-- - none: no targets allowed
local available_subdirs = {
{
id = "host",
label = "hosts",
target_type = "cidr",
}, {
id = "flow",
label = "flows",
target_type = "interface",
}, {
id = "interface",
label = "interfaces",
target_type = "interface",
}, {
id = "network",
label = "networks",
target_type = "network",
}, {
id = "snmp_device",
label = "host_details.snmp",
target_type = "cidr",
}, {
id = "system",
label = "system",
target_type = "none",
}, {
id = "syslog",
label = "Syslog",
target_type = "interface",
}
}
-- User scripts category consts
user_scripts.script_categories = {
other = {
icon = "fas fa-scroll",
i18n_title = "user_scripts.category_other",
i18n_descr = "user_scripts.category_other_descr",
},
security = {
icon = "fas fa-shield-alt",
i18n_title = "user_scripts.category_security",
i18n_descr = "user_scripts.category_security_descr",
},
internals = {
icon = "fas fa-wrench",
i18n_title = "user_scripts.category_internals",
i18n_descr = "user_scripts.category_internals_descr",
},
network = {
icon = "fas fa-network-wired",
i18n_title = "user_scripts.category_network",
i18n_descr = "user_scripts.category_network_descr",
},
system = {
icon = "fas fa-server",
i18n_title = "user_scripts.category_system",
i18n_descr = "user_scripts.category_system_descr",
}
}
-- Auto-assign an id to the categories
local cat_id = 1
for cat_k, cat_v in pairs(user_scripts.script_categories) do
cat_v["id"] = cat_id
cat_id = cat_id + 1
end
-- 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"},
subdirs = {"flow"},
}, traffic_element = {
parent_dir = "interface",
hooks = {"min", "5mins", "hour", "day"},
subdirs = {"interface", "host", "network"},
has_per_hook_config = true, -- Each hook has a separate configuration
}, snmp_device = {
parent_dir = "system",
hooks = {"snmpDevice", "snmpDeviceInterface"},
subdirs = {"snmp_device"},
}, system = {
parent_dir = "system",
hooks = {"min", "5mins", "hour", "day"},
subdirs = {"system"},
has_per_hook_config = true, -- Each hook has a separate configuration
default_config_only = true, -- Only the default configset can be used
}, syslog = {
parent_dir = "system",
hooks = {"handleEvent"},
subdirs = {"syslog"},
default_config_only = true, -- Only the default configset can be used
}
}
-- ##############################################
-- @brief Given a category found in a user script, this method checks whether the category is valid
-- and, if not valid, it assigns to the plugin a default category
local function checkCategory(category)
if not category or not category["id"] then
return user_scripts.script_categories.other
end
for cat_k, cat_v in pairs(user_scripts.script_categories) do
if category["id"] == cat_v["id"] then
return cat_v
end
end
return user_scripts.script_categories.other
end
-- ##############################################
-- @brief Given a subdir, returns the corresponding script type
function user_scripts.getScriptType(search_subdir)
for _, script_type in pairs(user_scripts.script_types) do
for _, subdir in pairs(script_type.subdirs) do
if(subdir == search_subdir) then
return(script_type)
end
end
end
-- Not found
return(nil)
end
-- ##############################################
-- 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 = plugins_utils.getRuntimePath() .. "/callbacks"
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 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)
ts_utils.append(schema_prefix .. ":num_calls", {ifid = ifid, user_script = modkey, subdir = subdir, num_calls = stats.tot_num_calls}, when)
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)
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 = {}
end
return(scripts_benchmarks)
end
-- ##############################################
local function init_user_script(user_script, mod_fname, full_path, plugin, script_type, subdir)
local user_scripts_templates = require("user_scripts_templates")
user_script.key = mod_fname
user_script.path = full_path
user_script.subdir = subdir
user_script.default_enabled = ternary(user_script.default_enabled == false, false, true --[[ a nil value means enabled ]])
user_script.source_path = plugins_utils.getUserScriptSourcePath(user_script.path)
user_script.plugin = plugin
user_script.script_type = script_type
user_script.edition = plugin.edition
user_script.category = checkCategory(user_script.category)
if(user_script.gui and user_script.gui.input_builder) then
user_script.template = user_scripts_templates[user_script.gui.input_builder]
if(user_script.template == nil) then
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Unknown template '%s' for user script '%s'", user_script.gui.input_builder, mod_fname))
end
-- Possibly localize the input title/description
if user_script.gui.input_title then
user_script.gui.input_title = i18n(user_script.gui.input_title) or user_script.gui.input_title
end
if user_script.gui.input_description then
user_script.gui.input_description = i18n(user_script.gui.input_description) or user_script.gui.input_description
end
end
if(user_script.template == nil) then
user_script.template = user_scripts_templates.default
end
-- Expand hooks
if(user_script.hooks["all"] ~= nil) then
local callback = user_script.hooks["all"]
user_script.hooks["all"] = nil
for _, hook in pairs(script_type.hooks) do
user_script.hooks[hook] = callback
end
end
end
-- ##############################################
local function loadAndCheckScript(mod_fname, full_path, plugin, script_type, subdir, return_all, scripts_filter, hook_filter)
local alerts_disabled = (not areAlertsEnabled())
local setup_ok = true
-- Recheck the edition as the demo mode may expire
if((plugin.edition == "pro" and (not ntop.isPro())) or
((plugin.edition == "enterprise" and (not ntop.isEnterprise())))) then
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping user script '%s' with '%s' edition", mod_fname, plugin.edition))
return(nil)
end
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Loading user script '%s'", mod_fname))
local user_script = dofile(full_path)
if(type(user_script) ~= "table") then
traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Loading '%s' failed", full_path))
return(nil)
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", mod_fname))
return(nil)
end
if((not return_all) and ((user_script.nedge_exclude and ntop.isnEdge()) or (user_script.nedge_only and (not ntop.isnEdge())))) then
return(nil)
end
if((not return_all) and (user_script.windows_exclude and ntop.isWindows())) then
return(nil)
end
if(table.empty(user_script.hooks)) then
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("No 'hooks' defined in user script '%s', skipping", mod_fname))
return(nil)
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, mod_fname))
return(nil)
end
end
if((not user_script.gui) or (not user_script.gui.i18n_title) or (not user_script.gui.i18n_description)) then
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Module '%s' does not define a gui", mod_fname))
end
-- Augument with additional attributes
init_user_script(user_script, mod_fname, full_path, plugin, script_type, subdir)
if((not return_all) and alerts_disabled and user_script.is_alert) then
return(nil)
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))
return(nil)
end
end
if(scripts_filter ~= nil) then
local script_ok = scripts_filter(user_script)
if(not script_ok) then
return(nil)
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))
return(nil)
end
return(user_script)
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 old_ifid = interface.getId()
options = options or {}
ifid = tonumber(ifid)
-- 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(tostring(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)
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)
local full_path = os_utils.fixPath(checks_dir .. "/" .. fname)
local plugin = plugins_utils.getUserScriptPlugin(full_path)
if(plugin == nil) then
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Skipping unknown user script '%s'", mod_fname))
goto next_module
end
if(rv.modules[mod_fname]) then
traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Skipping duplicate module '%s'", mod_fname))
goto next_module
end
local user_script = loadAndCheckScript(mod_fname, full_path, plugin, script_type, subdir, return_all, scripts_filter, hook_filter)
if(not user_script) then
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(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(tostring(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")
local plugin = plugins_utils.getUserScriptPlugin(full_path)
if(ntop.exists(full_path) and (plugin ~= nil)) then
local user_script = loadAndCheckScript(mod_fname, full_path, plugin, script_type, subdir)
return(user_script)
end
end
return(nil)
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
-- ##############################################
-- @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.listSubdirs()
local rv = {}
for _, subdir in ipairs(available_subdirs) do
local item = table.clone(subdir)
item.label = i18n(item.label) or item.label
rv[#rv + 1] = item
end
return(rv)
end
-- ##############################################
function user_scripts.getSubdirTargetType(search_subdir)
for _, subdir in pairs(available_subdirs) do
if(subdir.id == search_subdir) then
return(subdir.target_type)
end
end
return "none"
end
-- ##############################################
local function findConfigSet(configsets, name)
for id, configset in pairs(configsets) do
if(configset.name == name) then
return(configset)
end
end
return(nil)
end
-- ##############################################
local function getNewConfigSetId(configsets)
local max_id = -1
for i in pairs(configsets) do
max_id = math.max(max_id, tonumber(i))
end
return(max_id+1)
end
-- ##############################################
local function validateConfigsets(configsets)
local cur_targets = {}
-- Ensure that no duplicate target is set
for _, configset in pairs(configsets) do
for subdir, subdir_table in pairs(configset.targets) do
cur_targets[subdir] = cur_targets[subdir] or {}
for _, conf_target in ipairs(subdir_table) do
local is_v4 = isIPv4(conf_target)
local is_v6 = isIPv6(conf_target)
local conf_target_normalized = nil
if(is_v4 or is_v6) then
local address, prefix = splitNetworkPrefix(conf_target)
local max_prefixlen = ternary(is_v4, 32, 128)
if((prefix == nil) or (prefix >= max_prefixlen)) then
prefix = max_prefixlen
end
-- Normalize
conf_target_normalized = ntop.networkPrefix(address, prefix) .. "/" .. prefix
else
conf_target_normalized = conf_target
end
local existing_id = cur_targets[subdir][conf_target_normalized]
if(existing_id) then
return false, i18n("configsets.duplicate_target", {target = conf_target, confname1 = configsets[existing_id].name, confname2 = configset.name})
end
cur_targets[subdir][conf_target_normalized] = configset.id
end
end
end
return true
end
-- ##############################################
local function saveConfigsets(configsets)
local to_delete = ntop.getHashKeysCache(CONFIGSETS_KEY) or {}
local rv, err = validateConfigsets(configsets)
if(not rv) then
return rv, err
end
for _, configset in pairs(configsets) do
local k = string.format("%d", configset.id)
local v = json.encode(configset)
ntop.setHashCache(CONFIGSETS_KEY, k, v)
to_delete[k] = nil
end
for confid in pairs(to_delete) do
ntop.delHashCache(CONFIGSETS_KEY, confid)
end
-- Reload the periodic scripts as the configuration has changed
ntop.reloadPeriodicScripts()
return true
end
-- ##############################################
local cached_config_sets = nil
function user_scripts.getConfigsets()
if cached_config_sets then
return(cached_config_sets)
end
local configsets = ntop.getHashAllCache(CONFIGSETS_KEY) or {}
local rv = {}
for _, confset_json in pairs(configsets) do
local confset = json.decode(confset_json)
if confset then
rv[confset.id] = confset
end
end
-- Cache to avoid loading them again
cached_config_sets = rv
return(rv)
end
-- ##############################################
function user_scripts.deleteConfigset(confid)
confid = tonumber(confid)
if(confid == user_scripts.DEFAULT_CONFIGSET_ID) then
return false, "Cannot delete default configset"
end
local configsets = user_scripts.getConfigsets()
if(configsets[confid] == nil) then
return false, i18n("configsets.unknown_id", {confid=confid})
end
configsets[confid] = nil
return saveConfigsets(configsets)
end
-- ##############################################
function user_scripts.createOrReplaceConfigset(configset)
local configsets = user_scripts.getConfigsets()
local existing = findConfigSet(configsets, configset.name)
if existing then
configsets[existing.id] = nil
end
local new_confid = 0
if configset.id ~= 0 then
new_confid = getNewConfigSetId(configsets)
end
configsets[new_confid] = table.clone(configset)
configsets[new_confid].id = new_confid
local rv, err = saveConfigsets(configsets)
if not rv then
return rv, err
end
return true, new_confid
end
-- ##############################################
function user_scripts.renameConfigset(confid, new_name)
if(confid == user_scripts.DEFAULT_CONFIGSET_ID) then
return false, "Cannot rename default configset"
end
local configsets = user_scripts.getConfigsets()
if(configsets[confid] == nil) then
return false, i18n("configsets.unknown_id", {confid=confid})
end
local existing = findConfigSet(configsets, new_name)
if existing then
if(existing.id == confid) then
-- Renaming to the same name has no effect
return true
end
return false, i18n("configsets.error_exists", {name=new_name})
end
configsets[confid].name = new_name
return saveConfigsets(configsets)
end
-- ##############################################
function user_scripts.cloneConfigset(confid, new_name)
local configsets = user_scripts.getConfigsets()
if(configsets[confid] == nil) then
return false, i18n("configsets.unknown_id", {confid=confid})
end
local existing = findConfigSet(configsets, new_name)
if existing then
return false, i18n("configsets.error_exists", {name=new_name})
end
local new_confid = getNewConfigSetId(configsets)
configsets[new_confid] = table.clone(configsets[confid])
configsets[new_confid].id = new_confid
configsets[new_confid].name = new_name
configsets[new_confid].targets = {}
local rv, err = saveConfigsets(configsets)
if(not rv) then
return rv, err
end
return true, new_confid
end
-- ##############################################
function user_scripts.setConfigsetTargets(subdir, confid, targets)
local configsets = user_scripts.getConfigsets()
if(configsets[confid] == nil) then
return false, i18n("configsets.unknown_id", {confid=confid})
end
if(confid == user_scripts.DEFAULT_CONFIGSET_ID) then
return false, "Cannot set target on the default configuration"
end
-- Update the targets
configsets[confid].targets[subdir] = targets
return saveConfigsets(configsets)
end
-- ##############################################
-- @brief Update the configuration of a specific script in a configset
function user_scripts.updateScriptConfig(confid, script_key, subdir, new_config)
local configsets = user_scripts.getConfigsets()
new_config = new_config or {}
local applied_config = {}
if(configsets[confid] == nil) then
return false, i18n("configsets.unknown_id", {confid=confid})
end
local script_type = user_scripts.getScriptType(subdir)
local script = user_scripts.loadModule(interface.getId(), script_type, subdir, script_key)
if(script) then
-- Try to validate the configuration
for hook, conf in pairs(new_config) do
local valid = true
local rv_or_err = ""
if(conf.enabled == nil) then
return false, "Missing 'enabled' item"
end
if(conf.script_conf == nil) then
return false, "Missing 'script_conf' item"
end
if conf.enabled then
valid, rv_or_err = script.template:parseConfig(script, conf.script_conf)
else
-- Assume the config is valid when the script is disabled to simplify the check
valid = true
rv_or_err = conf.script_conf
end
if(not valid) then
return false, rv_or_err
end
-- The validator may have changed the configuration
conf.script_conf = rv_or_err
applied_config[hook] = conf
end
end
local config = configsets[confid].config
config[subdir] = config[subdir] or {}
config[subdir][script_key] = applied_config
return saveConfigsets(configsets)
end
-- ##############################################
function user_scripts.toggleScript(confid, script_key, subdir, enable)
local configsets = user_scripts.getConfigsets()
local configset = configsets[confid]
if(configset == nil) then
return false, i18n("configsets.unknown_id", {confid=confid})
end
local script_type = user_scripts.getScriptType(subdir)
local script = user_scripts.loadModule(interface.getId(), script_type, subdir, script_key)
if(script == nil) then
return false, i18n("configsets.unknown_user_script", {user_script=script_key})
end
local config = user_scripts.getScriptConfig(configset, script, subdir)
if(config == nil) then
return false
end
for _, hook in pairs(config) do
hook.enabled = enable
end
return saveConfigsets(configsets)
end
-- ##############################################
function user_scripts.loadDefaultConfig()
local ifid = getSystemInterfaceId()
local configsets = user_scripts.getConfigsets()
local default_conf = configsets[user_scripts.DEFAULT_CONFIGSET_ID]
if default_conf then
default_conf = default_conf.config or {}
-- Drop possible nested values due to a previous bug
default_conf.config = nil
else
default_conf = {}
end
for type_id, script_type in pairs(user_scripts.script_types) do
for _, subdir in pairs(script_type.subdirs) do
local scripts = user_scripts.load(ifid, script_type, subdir, {return_all = true})
for key, usermod in pairs(scripts.modules) do
if((usermod.default_enabled ~= nil) or (usermod.default_value ~= nil)) then
default_conf[subdir] = default_conf[subdir] or {}
default_conf[subdir][key] = default_conf[subdir][key] or {}
local script_config = default_conf[subdir][key]
local hooks = ternary(script_type.has_per_hook_config, usermod.hooks, {[ALL_HOOKS_CONFIG_KEY]=1})
for hook in pairs(hooks) do
-- Do not override an existing configuration
if(script_config[hook] == nil) then
script_config[hook] = {
enabled = usermod.default_enabled or false,
script_conf = usermod.default_value or {},
}
end
end
end
end
end
end
configsets[user_scripts.DEFAULT_CONFIGSET_ID] = {
id = user_scripts.DEFAULT_CONFIGSET_ID,
name = i18n("policy_presets.default"),
config = default_conf,
targets = {},
}
saveConfigsets(configsets)
end
-- ##############################################
function user_scripts.resetConfigsets()
cached_config_sets = nil
ntop.delCache(CONFIGSETS_KEY)
user_scripts.loadDefaultConfig()
return(true)
end
-- ##############################################
-- Returns true if a system script is enabled for some hook
function user_scripts.isSystemScriptEnabled(script_key)
-- Verify that the script is currently available
local k = "ntonpng.cache.user_scripts.available_system_modules." .. script_key
local available = ntop.getCache(k)
if(isEmptyString(available)) then
local m = user_scripts.loadModule(getSystemInterfaceId(), user_scripts.script_types.system, "system", script_key)
available = (m ~= nil)
ntop.setCache(k, ternary(available, "1", "0"))
else
available = ternary(available == "1", true, false)
end
if(not available) then
return(false)
end
local configsets = user_scripts.getConfigsets()
local default_config = user_scripts.getDefaultConfig(configsets, "system")
local script_config = default_config[script_key]
if(script_config) then
for _, hook in pairs(script_config) do
if(hook.enabled) then
return(true)
end
end
end
return(false)
end
-- ##############################################
local default_config = {
enabled = false,
script_conf = {},
}
-- @brief Retrieves the configuration of a specific script
function user_scripts.getScriptConfig(configset, script, subdir)
local script_key = script.key
local config = configset.config[subdir]
if(config[script_key]) then
-- A configuration was found
return(config[script_key])
end
-- Default
local rv = {}
local script_type = user_scripts.getScriptType(subdir)
local hooks = ternary(script_type.has_per_hook_config, script.hooks, {[ALL_HOOKS_CONFIG_KEY]=1})
for hook in pairs(script.hooks) do
rv[hook] = default_config
end
return(rv)
end
-- ##############################################
-- @brief Retrieves the configuration of a specific hook of the target
-- @param target_config target configuration as returned by
-- user_scripts.getTargetConfig/user_scripts.getHostTargetConfigset
function user_scripts.getTargetHookConfig(target_config, script, hook)
local script_conf = target_config[script.key or script]
if not hook then
-- See has_per_hook_config
hook = ALL_HOOKS_CONFIG_KEY
end
if(not script_conf) then
return(default_config)
end
return(script_conf[hook] or default_config)
end
-- ##############################################
local fast_target_lookup = nil
-- NOTE: this only works for exact searches. For hosts see user_scripts.getHostTargetConfigset
function user_scripts.getTargetConfig(configsets, subdir, target)
if(fast_target_lookup == nil) then
fast_target_lookup = {}
for _, configset in pairs(configsets) do
for _, conf_target in pairs(configset.targets[subdir] or {}) do
fast_target_lookup[conf_target] = configset
end
end
end
local conf = fast_target_lookup[target] or configsets[user_scripts.DEFAULT_CONFIGSET_ID]
if(conf == nil) then
return({})
end
return conf.config[subdir] or {}, conf.id
end
-- ##############################################
function user_scripts.getDefaultConfig(configsets, subdir)
local conf = configsets[user_scripts.DEFAULT_CONFIGSET_ID]
if(conf == nil) then
return({})
end
return conf.config[subdir] or {}, conf.id
end
-- ##############################################
local host_confsets_ptree_initialized = false
-- Performs an IP based match by using a patricia tree
function user_scripts.getHostTargetConfigset(configsets, subdir, ip_target)
if(not host_confsets_ptree_initialized) then
-- Start with an empty ptree
ntop.ptreeClear()
for _, configset in pairs(configsets) do
for _, conf_target in pairs(configset.targets[subdir] or {}) do
ntop.ptreeInsert(conf_target, configset.id)
end
end
host_confsets_ptree_initialized = true
end
local match_id = ntop.ptreeMatch(ip_target) or user_scripts.DEFAULT_CONFIGSET_ID
local conf = configsets[match_id]
if(conf == nil) then
return({})
end
return conf.config[subdir] or {}, conf.id
end
-- ##############################################
function user_scripts.getScriptEditorUrl(script)
if(script.edition == "community") then
local plugin_file_path = string.sub(script.source_path, string.len(dirs.scriptdir) + 1)
local plugin_path = string.sub(script.plugin.path, string.len(dirs.scriptdir) + 1)
return(string.format("%s/lua/code_viewer.lua?plugin_file_path=%s&plugin_path=%s", ntop.getHttpPrefix(), plugin_file_path, plugin_path))
end
return(nil)
end
-- ##############################################
return(user_scripts)