Software /
code /
prosody
Changeset
13707:6c59b9072871 13.0
prosodyctl: check features: Check for recommended feature availability
Inspired by mod_compliance_*, this command will help people (especially those
with older configs, upgrading from previous releases) learn what features
their Prosody configuration may be missing.
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Sat, 15 Feb 2025 16:34:16 +0000 |
parents | 13706:a988867a5567 |
children | 13708:9f8e9aabc00b |
files | util/prosodyctl/check.lua |
diffstat | 1 files changed, 232 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- a/util/prosodyctl/check.lua Sat Feb 15 16:31:10 2025 +0000 +++ b/util/prosodyctl/check.lua Sat Feb 15 16:34:16 2025 +0000 @@ -325,7 +325,12 @@ local ok = true; local function contains_match(hayset, needle) for member in hayset do if member:find(needle) then return true end end end local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end + local function is_user_host(host, conf) return host ~= "*" and conf.component_module == nil; end + local function is_component_host(host, conf) return host ~= "*" and conf.component_module ~= nil; end local function enabled_hosts() return it.filter(disabled_hosts, it.sorted_pairs(configmanager.getconfig())); end + local function enabled_user_hosts() return it.filter(is_user_host, it.sorted_pairs(configmanager.getconfig())); end + local function enabled_components() return it.filter(is_component_host, it.sorted_pairs(configmanager.getconfig())); end + local checks = {}; function checks.disabled() local disabled_hosts_set = set.new(); @@ -802,6 +807,22 @@ end end + -- Check features + do + local missing_features = {}; + for host in enabled_user_hosts() do + local all_features = checks.features(host, true); + if not all_features then + table.insert(missing_features, host); + end + end + if #missing_features > 0 then + print(""); + print(" Some of your hosts may be missing features due to a lack of configuration."); + print(" For more details, use the 'prosodyctl check features' command."); + end + end + print("Done.\n"); end function checks.dns() @@ -1450,6 +1471,217 @@ end end end + + function checks.features(host, quiet) + if not quiet then + print("Feature report"); + end + + local common_subdomains = { + http_file_share = "share"; + muc = "groups"; + }; + + local function print_feature_status(feature, host) + if quiet then return; end + print("", feature.ok and "OK" or "(!)", feature.name); + if not feature.ok then + if feature.lacking_modules then + table.sort(feature.lacking_modules); + print("", "", "Suggested modules: "); + for _, module in ipairs(feature.lacking_modules) do + print("", "", (" - %s: https://prosody.im/doc/modules/mod_%s"):format(module, module)); + end + end + if feature.lacking_components then + table.sort(feature.lacking_components); + for _, component_module in ipairs(feature.lacking_components) do + local subdomain = common_subdomains[component_module]; + if subdomain then + print("", "", "Suggested component:"); + print(""); + print("", "", "", ("Component %q %q"):format(subdomain.."."..host, component_module)); + print("", "", "", ("-- Documentation: https://prosody.im/doc/modules/mod_%s"):format(component_module)); + else + print("", "", ("Suggested component: %s"):format(component_module)); + end + end + print(""); + print("", "", "If you have already configured any these components, they may not be"); + print("", "", "linked correctly to "..host..". For more info see https://prosody.im/doc/components"); + end + end + print(""); + end + + local all_ok = true; + + local config = configmanager.getconfig(); + + local f, s, v; + if check_host then + f, s, v = it.values({ check_host }); + else + f, s, v = enabled_user_hosts(); + end + + for host in f, s, v do + local modules_enabled = set.new(config["*"].modules_enabled); + modules_enabled:include(set.new(config[host].modules_enabled)); + + -- { [component_module] = { hostname1, hostname2, ... } } + local host_components = setmetatable({}, { __index = function (t, k) return rawset(t, k, {})[k]; end }); + + do + local hostapi = api(host); + + -- Find implicitly linked components + for other_host in enabled_components() do + local parent_host = other_host:match("^[^.]+%.(.+)$"); + if parent_host == host then + local component_module = configmanager.get(other_host, "component_module"); + if component_module then + table.insert(host_components[component_module], other_host); + end + end + end + + -- And components linked explicitly + for _, disco_item in ipairs(hostapi:get_option_array("disco_items", {})) do + local other_host = disco_item[1]; + local component_module = configmanager.get(other_host, "component_module"); + if component_module then + table.insert(host_components[component_module], other_host); + end + end + end + + local current_feature; + + local function check_module(suggested, alternate, ...) + if set.intersection(modules_enabled, set.new({suggested, alternate, ...})):empty() then + current_feature.lacking_modules = current_feature.lacking_modules or {}; + table.insert(current_feature.lacking_modules, suggested); + end + end + + local function check_component(suggested, alternate, ...) + local found; + for _, component_module in ipairs({ suggested, alternate, ... }) do + found = #host_components[component_module] > 0; + if found then break; end + end + if not found then + current_feature.lacking_components = current_feature.lacking_components or {}; + table.insert(current_feature.lacking_components, suggested); + end + end + + local features = { + { + name = "Basic functionality"; + check = function () + check_module("disco"); + check_module("roster"); + check_module("saslauth"); + check_module("tls"); + check_module("pep"); + end; + }; + { + name = "Multi-device sync"; + check = function () + check_module("carbons"); + check_module("mam"); + check_module("bookmarks"); + end; + }; + { + name = "Mobile optimizations"; + check = function () + check_module("smacks"); + check_module("csi_simple", "csi_battery_saver"); + end; + }; + { + name = "Web connections"; + check = function () + check_module("bosh"); + check_module("websocket"); + end; + }; + { + name = "User profiles"; + check = function () + check_module("vcard_legacy", "vcard"); + end; + }; + { + name = "Blocking"; + check = function () + check_module("blocklist"); + end; + }; + { + name = "Push notifications"; + check = function () + check_module("cloud_notify"); + end; + }; + { + name = "Audio/video calls"; + check = function () + check_module( + "turn_external", + "external_services", + "turncredentials", + "extdisco" + ); + end; + }; + { + name = "File sharing"; + check = function () + check_component("http_file_share", "http_upload"); + end; + }; + { + name = "Group chats"; + check = function () + check_component("muc"); + end; + }; + }; + + if not quiet then + print(host); + end + + for _, feature in ipairs(features) do + current_feature = feature; + feature.check(); + feature.ok = not feature.lacking_modules and not feature.lacking_components; + -- For improved presentation, we group the (ok) and (not ok) features + if feature.ok then + print_feature_status(feature, host); + end + end + + for _, feature in ipairs(features) do + if not feature.ok then + all_ok = false; + print_feature_status(feature, host); + end + end + + if not quiet then + print(""); + end + end + + return all_ok; + end + if what == nil or what == "all" then local ret; ret = checks.disabled();