Use new user scripts config and gui

The user scripts configuration can now be configured from the "User Scripts" entry under the cog
icon. It allows the creation of multiple configuration presets to be applied to hosts, networks and
interfaces.
This commit is contained in:
emanuele-f 2020-01-03 13:01:20 +01:00
parent 538ebc741a
commit d037f9a9a4
18 changed files with 533 additions and 542 deletions

View file

@ -0,0 +1,272 @@
function drawAlertSourceSettings(entity_type, alert_source, delete_button_msg, delete_confirm_msg, page_name, page_params, alt_name, show_entity, options)
options = options or {}
local tab = _GET["tab"] or "min"
local ts_utils = require("ts_utils")
local ifid = interface.getId()
local entity_value = alert_source
local subdir = entity_type
if interface.isPcapDumpInterface() then
if entity_type == "interface" then
tab = "flows"
else
return
end
else
local function printTab(tab, content, sel_tab)
if(tab == sel_tab) then print("\t<li class='nav-item active show'>") else print("\t<li class='nav-item'>") end
print("<a class='nav-link' href=\""..ntop.getHttpPrefix().."/lua/"..page_name.."?page=callbacks&tab="..tab)
for param, value in pairs(page_params) do
print("&"..param.."="..value)
end
print("\">"..content.."</a></li>\n")
end
print('<ul class="nav nav-tabs">')
for k, granularity in pairsByField(alert_consts.alerts_granularities, "granularity_id", asc) do
local l = i18n(granularity.i18n_title)
local resolution = granularity.granularity_seconds
if (not options.remote_host) or resolution <= 60 then
--~ l = '<i class="fas fa-cog" aria-hidden="true"></i>&nbsp;'..l
printTab(k, l, tab)
end
end
if(entity_type == "interface") then
local l = i18n("flows")
printTab("flows", l, tab)
end
if tab ~= "flows" then
local granularity_label = alertEngineLabel(alertEngine(tab))
print(
template.gen("modal_confirm_dialog.html", {
dialog={
id = "deleteAlertSourceSettings",
action = "deleteAlertSourceSettings()",
title = i18n("show_alerts.delete_alerts_configuration"),
message = i18n(delete_confirm_msg, {granularity=granularity_label}) .. " <span style='white-space: nowrap;'>" .. ternary(alt_name ~= nil, alt_name, alert_source).."</span>?",
confirm = i18n("delete")
}
})
)
print(
template.gen("modal_confirm_dialog.html", {
dialog={
id = "deleteGlobalAlertConfig",
action = "deleteGlobalAlertConfig()",
title = i18n("show_alerts.delete_alerts_configuration"),
message = i18n("show_alerts.delete_config_message", {conf = entity_type, granularity=granularity_label}).."?",
confirm = i18n("delete")
}
})
)
end
print('</ul>')
end -- !isPcapDumpInterface
if((tab == "flows") and (entity_type == "interface")) then
local flow_callbacks_utils = require "flow_callbacks_utils"
flow_callbacks_utils.print_callbacks_config()
else
local available_modules = user_scripts.load(interface.getId(), user_scripts.script_types.traffic_element, entity_type)
local no_modules_available = table.len(available_modules.modules) == 0
if((_POST["to_delete"] ~= nil) and (_POST["SaveAlerts"] == nil)) then
if _POST["to_delete"] == "local" then
user_scripts.deleteSpecificConfiguration(subdir, available_modules, tab, entity_value)
else
user_scripts.deleteGlobalConfiguration(subdir, available_modules, tab, options.remote_host)
end
elseif(not table.empty(_POST)) then
user_scripts.handlePOST(subdir, available_modules, tab, entity_value, options.remote_host)
end
local label
if entity_type == "host" then
if options.remote_host then
label = i18n("remote_hosts")
else
label = i18n("alerts_thresholds_config.active_local_hosts")
end
else
label = firstToUpper(entity_type) .. "s"
end
print [[
</ul>
<form method="post">
<br>
<table id="user" class="table table-bordered table-striped" style="clear: both"> <tbody>
<tr><th width"40%">]] print(i18n("alerts_thresholds_config.threshold_type")) print[[</th>]]
if(tab == "min") then
print[[<th class="text-center" width=5%>]] print(i18n("chart")) print[[</th>]]
end
print[[<th width=20%>]] print(i18n("alerts_thresholds_config.thresholds_single_source", {source=firstToUpper(entity_type),alt_name=ternary(alt_name ~= nil, alt_name, alert_source)})) print[[</th><th width=20%>]] print(i18n("alerts_thresholds_config.common_thresholds_local_sources", {source=label}))
print[[</th><th style="text-align: center;">]] print(i18n("flow_callbacks.callback_latest_run")) print[[</th></tr>]]
print('<input id="csrf" name="csrf" type="hidden" value="'..ntop.getRandomCSRFValue()..'" />\n')
if no_modules_available then
if areAlertsEnabled() then
print[[<tr><td colspan=5>]] print(i18n("flow_callbacks.no_callbacks_available")) print[[.</td></tr>]]
else
print[[<tr><td colspan=5>]] print(i18n("flow_callbacks.no_callbacks_available_disabled_alerts", {url = ntop.getHttpPrefix().."/lua/admin/prefs.lua?tab=alerts"})) print[[.</td></tr>]]
end
else
local benchmarks = user_scripts.getLastBenchmark(ifid, entity_type)
for mod_k, user_script in pairsByKeys(available_modules.modules, asc) do
local key = user_script.key
local gui_conf = user_script.gui
local show_input = true
if user_script.granularity then
-- check if the check is performed and thus has to
-- be configured at this granularity
show_input = false
for _, gran in pairs(user_script.granularity) do
if gran == tab then
show_input = true
break
end
end
end
if(user_script.local_only and options.remote_host) then
show_input = false
end
if not gui_conf or not show_input then
goto next_module
end
local url = ''
if(user_script.plugin.edition == "community") then
local path = string.sub(user_script.source_path, string.len(ntop.getDirs().scriptdir)+1)
url = '<A HREF="/lua/code_viewer.lua?lua_script_path='..path..'"><i class="fas fa-lg fa-binoculars"></i></A>'
end
print("<tr><td><b>".. (i18n(gui_conf.i18n_title) or gui_conf.i18n_title) .. " " .. url .."</b><br>")
print("<small>".. (i18n(gui_conf.i18n_description) or gui_conf.i18n_description) .."</small>\n")
if(tab == "min") then
print("<td class='text-center'>")
if ts_utils.exists("elem_user_script:duration", {ifid=ifid, user_script=mod_k, subdir=entity_type}) then
print('<a href="'.. ntop.getHttpPrefix() ..'/lua/user_script_details.lua?ifid='..ifid..'&user_script='..
mod_k..'&subdir='..entity_type..'"><i class="fas fa-chart-area fa-lg"></i></a>')
end
end
for _, prefix in pairs({"", "global_"}) do
if user_script.gui.input_builder then
local k = prefix..key
local is_global = (prefix == "global_")
local conf
print("</td><td>")
if is_global then
conf = user_scripts.getGlobalConfiguration(user_script, tab, options.remote_host)
else
conf = user_scripts.getConfiguration(user_script, tab, entity_value, options.remote_host)
end
if(conf ~= nil) then
-- TODO remove after implementing the new gui
local value = ternary(user_script.gui.post_handler == user_scripts.checkbox_post_handler, conf.enabled, conf.script_conf)
print(user_script.gui.input_builder(user_script.gui or {}, k, value))
end
end
end
print("</td><td align='center'>\n")
local script_benchmark = benchmarks[mod_k]
if script_benchmark and (script_benchmark[tab] or script_benchmark["all"]) then
local hook = ternary(script_benchmark[tab], tab, "all")
if script_benchmark[hook]["tot_elapsed"] then
if script_benchmark[hook]["tot_num_calls"] > 1 then
print(i18n("flow_callbacks.callback_function_duration_fmt_long",
{num_calls = format_utils.formatValue(script_benchmark[hook]["tot_num_calls"]),
time = format_utils.secondsToTime(script_benchmark[hook]["tot_elapsed"]),
speed = format_utils.formatValue(round(script_benchmark[hook]["avg_speed"], 0))}))
else
print(i18n("flow_callbacks.callback_function_duration_fmt_short",
{time = format_utils.secondsToTime(script_benchmark[hook]["tot_elapsed"])}))
end
end
end
print("</td></tr>\n")
::next_module::
end
end
print [[</tbody> </table>]]
if not no_modules_available then
print[[
<input type="hidden" name="SaveAlerts" value="">
<button class="btn btn-primary" style="float:right; margin-right:1em;" disabled="disabled" type="submit">]] print(i18n("save_configuration")) print[[</button>
</form>
<button type="button" class="btn btn-secondary" data-toggle="modal" data-target="#deleteGlobalAlertConfig" style="float:right; margin-right:1em;"> ]] print(i18n("show_alerts.delete_config_btn",{conf=firstToUpper(entity_type)})) print[[</button>
<button type="button" class="btn btn-secondary" data-toggle="modal" data-target="#deleteAlertSourceSettings" style="float:right; margin-right:1em;"> ]] print(delete_button_msg) print[[</button>
]]
end
print("<div style='margin-top:4em;'><b>" .. i18n("alerts_thresholds_config.notes") .. ":</b><ul>")
print("<li>" .. i18n("alerts_thresholds_config.note_control_threshold_checks_periods") .. "</li>")
print("<li>" .. i18n("alerts_thresholds_config.note_thresholds_expressed_as_delta") .. "</li>")
print("<li>" .. i18n("alerts_thresholds_config.note_consecutive_checks") .. "</li>")
if (entity_type == "host") then
print("<li>" .. i18n("alerts_thresholds_config.note_checks_on_active_hosts") .. "</li>")
end
print("<li>" .. i18n("alerts_thresholds_config.note_create_custom_scripts", {url = "https://github.com/ntop/ntopng/blob/dev/doc/README.alerts.md"}) .. "</li>")
print("<li>" .. i18n("flow_callbacks.note_add_custom_scripts", {url = ntop.getHttpPrefix().."/lua/directories.lua", product=ntop.getInfo()["product"]}) .. "</li>")
print("<li>" .. i18n("flow_callbacks.note_scripts_list", {url = ntop.getHttpPrefix().."/lua/user_scripts_overview.lua", product=ntop.getInfo()["product"]}) .. "</li>")
print("</ul></div>")
print[[
<script>
function deleteAlertSourceSettings() {
var params = {};
params.to_delete = "local";
params.csrf = "]] print(ntop.getRandomCSRFValue()) print[[";
var form = paramsToForm('<form method="post"></form>', params);
form.appendTo('body').submit();
}
function deleteGlobalAlertConfig() {
var params = {};
params.to_delete = "global";
params.csrf = "]] print(ntop.getRandomCSRFValue()) print[[";
var form = paramsToForm('<form method="post"></form>', params);
form.appendTo('body').submit();
}
aysHandleForm("form", {
handle_tabs: true,
});
</script>
]]
end
end

View file

@ -0,0 +1,239 @@
--
-- (C) 2014-19 - ntop.org
--
local dirs = ntop.getDirs()
package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path
local alerts_api = require "alerts_api"
local format_utils = require "format_utils"
local user_scripts = require "user_scripts"
local ts_utils = require("ts_utils")
local flow_callbacks_utils = {}
local ifid = interface.getId()
-- ##############################################
local function printStatsCols(total_stats, cur_elapsed, cur_num_calls, cur_avg_speed)
print("<td align='center'>".. format_utils.secondsToTime(cur_elapsed) .." (" .. string.format("%.1f", cur_elapsed * 100 / total_stats.tot_elapsed) .. "%)</td>")
print("<td align='center'>".. format_utils.formatValue(cur_num_calls) .." (" .. string.format("%.1f", cur_num_calls * 100 / total_stats.tot_num_calls) .. "%)</td>")
print("<td align='center'>".. format_utils.formatValue(round(cur_avg_speed, 0)) .."</td>")
end
-- ##############################################
local function print_callbacks_config_table(descr, expert_view)
print[[<table id="callbacks_config_table" class="table table-bordered table-striped">]]
print[[<tr>]]
print[[<th width=40%>]] print(i18n("flow_callbacks.callback")) print[[</th>]]
print[[<th style="text-align: center;" width="5%">]] print(i18n("chart")) print[[</th>]]
print[[<th width="20%">]] print(i18n("flow_callbacks.callback_config")) print[[</th>]]
if(expert_view) then
print[[<th>]] print(i18n("flow_callbacks.callback_function")) print[[</th>]]
end
print[[<th style="text-align: center;">]] print(i18n("flow_callbacks.last_duration")) print[[</th>]]
print[[<th style="text-align: center;">]] print(i18n("flow_callbacks.last_num_calls")) print[[</th>]]
print[[<th style="text-align: center;">]] print(i18n("flow_callbacks.last_calls_per_sec")) print[[</th>]]
print[[</tr>]]
print('<input id="csrf" name="csrf" type="hidden" value="'..ntop.getRandomCSRFValue()..'" />\n')
local total_stats = {
tot_elapsed = 0,
tot_num_calls = 0,
}
local benchmarks = user_scripts.getLastBenchmark(ifid, "flow")
-- A user module link is currently required for the total
local total_user_module = nil
for mod_k, user_script in pairsByKeys(descr.modules, asc) do
local hooks_benchmarks = benchmarks[mod_k] or {}
-- Calculate the total stats
for _, mod_benchmark in pairs(hooks_benchmarks) do
total_stats.tot_elapsed = total_stats.tot_elapsed + mod_benchmark["tot_elapsed"]
total_stats.tot_num_calls = total_stats.tot_num_calls + mod_benchmark["tot_num_calls"]
end
end
for mod_k, user_script in pairsByKeys(descr.modules, asc) do
local hooks_benchmarks = benchmarks[mod_k] or {}
local num_hooks = table.len(hooks_benchmarks)
local title
local description
local rowspan = ""
if(not total_user_module) then
total_user_module = mod_k
end
if(user_script.gui) then
title = i18n(user_script.gui.i18n_title) or user_script.gui.i18n_title
description = i18n(user_script.gui.i18n_description) or user_script.gui.i18n_description
else
title = user_script.key
description = ""
end
if(expert_view and (num_hooks > 0)) then
rowspan = string.format(' rowspan="%d"', num_hooks)
end
local url = ''
if(user_script.edition == "community") then
local path = string.sub(user_script.source_path, string.len(ntop.getDirs().scriptdir)+1)
url = '<A HREF="/lua/code_viewer.lua?lua_script_path='.. path ..'"><i class="fas fa-lg fa-binoculars"></i></A>'
end
print("<tr><td ".. rowspan .."><b>".. title .." "..url.."</b><br>")
print("<small>"..description.."</small></td>")
print("<td ".. rowspan .." class='text-center'>")
if(ts_utils.exists("flow_user_script:duration", {ifid=ifid, user_script=mod_k, subdir="flow"})) then
print('<a href="'.. ntop.getHttpPrefix() ..'/lua/user_script_details.lua?ifid='..ifid..'&user_script='..mod_k..'&subdir=flow"><i class="fas fa-chart-area fa-lg" data-original-title="" title=""></i></a>')
end
print("</td>")
print("<td ".. rowspan ..">")
if(user_script.gui and user_script.gui.input_builder) then
local conf = user_scripts.getConfiguration(user_script)
local k = user_script.key
-- TODO remove after implementing the new gui
local value = ternary(user_script.gui.post_handler == user_scripts.checkbox_post_handler, conf.enabled, conf.script_conf)
print(user_script.gui.input_builder(user_script.gui or {}, k, value))
else
print('<a href="'.. ntop.getHttpPrefix() ..'/lua/admin/prefs.lua?tab=alerts"><i class="fas fa-flask fa-lg"></i></a>')
end
print("</td>")
if(expert_view) then
if(num_hooks > 0) then
local ctr = 0
for mod_fn, mod_benchmark in pairsByKeys(hooks_benchmarks, asc) do
print("<td>".. mod_fn .."</td>")
local avg_speed = (mod_benchmark["tot_num_calls"] / mod_benchmark["tot_elapsed"])
printStatsCols(total_stats, mod_benchmark["tot_elapsed"], mod_benchmark["tot_num_calls"], avg_speed)
ctr = ctr + 1
if(ctr ~= num_hooks) then
print("</tr><tr>")
end
end
else
print("<td></td><td></td><td></td><td></td>")
end
else
if(num_hooks > 0) then
-- Accumulate the stats for each hook
local total_duration = 0
local num_calls = 0
for mod_fn, mod_benchmark in pairsByKeys(hooks_benchmarks, asc) do
total_duration = total_duration + mod_benchmark["tot_elapsed"]
num_calls = num_calls + mod_benchmark["tot_num_calls"]
end
local avg_speed = (num_calls / total_duration)
printStatsCols(total_stats, total_duration, num_calls, avg_speed)
else
print("<td></td><td></td><td></td>")
end
end
end
local avg_speed = (total_stats.tot_num_calls / total_stats.tot_elapsed)
-- Print total stats
print("</tr><tr><td><b>" .. i18n("total") .. "</b></td><td class='text-center'>")
if(ts_utils.exists("flow_user_script:total_stats", {ifid=ifid, subdir="flow"})) then
print('<a href="'.. ntop.getHttpPrefix() ..'/lua/user_script_details.lua?ifid='..ifid..'&subdir=flow&user_script='.. total_user_module ..'&ts_schema=custom:flow_user_script:total_stats"><i class="fas fa-chart-area fa-lg"></i></a>')
end
print("<td>")
if(expert_view) then
print("<td></td>")
end
print("</tr>")
print[[</table>]]
end
-- #################################
function flow_callbacks_utils.print_callbacks_config()
local show_advanced_prefs = false
if(_GET["show_advanced_prefs"] == "1") then
show_advanced_prefs = true
end
local ifid = interface.getId()
local descr = user_scripts.load(ifid, user_scripts.script_types.flow, "flow")
print [[
<br>]]
if table.len(_POST) > 0 then
user_scripts.handlePOST("flow", descr)
end
print[[<form id="flow-callbacks-config" class="form-inline" method="post">]]
print_callbacks_config_table(descr, show_advanced_prefs)
print[[<input type=hidden name="show_advanced_prefs" value="]]if show_advanced_prefs then print("true") else print("false") end print[["/>]]
print[[<button class="btn btn-primary" style="float:right; margin-right:1em; margin-left:auto" type="submit">]] print(i18n("save_configuration")) print[[</button>]]
print[[</form>]]
print[[
<form data-ays-ignore="true">
<input type=hidden name="page" value="callbacks" />
<input type=hidden name="tab" value="flows" />
<input type=hidden name="ifid" value="]] print(string.format("%d", ifid)) print[[" />
<input type=hidden name="show_advanced_prefs" value="]]if show_advanced_prefs then print("0") else print("1") end print[["/>
<div class="btn-group btn-toggle">
]]
local cls_on = "btn btn-sm"
local cls_off = cls_on
if show_advanced_prefs then
cls_on = cls_on..' btn-primary active'
cls_off = cls_off..' btn-secondary'
else
cls_on = cls_on..' btn-secondary'
cls_off = cls_off..' btn-primary active'
end
print('<button type="submit" class="'..cls_on..'">'..i18n("prefs.expert_view")..'</button>')
print('<button type="submit" class="'..cls_off..'">'..i18n("prefs.simple_view")..'</button>')
print[[
</div>
</form>
<script>
</script>
]]
print("<div style='margin-top:4em;'><b>" .. i18n("flow_callbacks.notes") .. ":</b><ul>")
print("<li>" .. i18n("flow_callbacks.note_flow_lifecycle") .. "</li>")
print("<li>" .. i18n("flow_callbacks.note_create_custom_scripts", {url = "https://github.com/ntop/ntopng/blob/dev/doc/README.developers.flow_lua_callbacks.md"}) .. "</li>")
print("<li>" .. i18n("flow_callbacks.note_add_custom_scripts", {url = ntop.getHttpPrefix().."/lua/directories.lua", product=ntop.getInfo()["product"]}) .. "</li>")
print("</ul></div>")
end
return flow_callbacks_utils

View file

@ -0,0 +1,182 @@
-- ##############################################
-- @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
-- ##############################################
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
-- Reload the periodic scripts as the configuration has changed
ntop.reloadPeriodicScripts()
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
-- ##############################################
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