Comparison

util/prosodyctl/check.lua @ 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
parent 13706:a988867a5567
child 13708:9f8e9aabc00b
comparison
equal deleted inserted replaced
13706:a988867a5567 13707:6c59b9072871
323 local set = require "prosody.util.set"; 323 local set = require "prosody.util.set";
324 local it = require "prosody.util.iterators"; 324 local it = require "prosody.util.iterators";
325 local ok = true; 325 local ok = true;
326 local function contains_match(hayset, needle) for member in hayset do if member:find(needle) then return true end end end 326 local function contains_match(hayset, needle) for member in hayset do if member:find(needle) then return true end end end
327 local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end 327 local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end
328 local function is_user_host(host, conf) return host ~= "*" and conf.component_module == nil; end
329 local function is_component_host(host, conf) return host ~= "*" and conf.component_module ~= nil; end
328 local function enabled_hosts() return it.filter(disabled_hosts, it.sorted_pairs(configmanager.getconfig())); end 330 local function enabled_hosts() return it.filter(disabled_hosts, it.sorted_pairs(configmanager.getconfig())); end
331 local function enabled_user_hosts() return it.filter(is_user_host, it.sorted_pairs(configmanager.getconfig())); end
332 local function enabled_components() return it.filter(is_component_host, it.sorted_pairs(configmanager.getconfig())); end
333
329 local checks = {}; 334 local checks = {};
330 function checks.disabled() 335 function checks.disabled()
331 local disabled_hosts_set = set.new(); 336 local disabled_hosts_set = set.new();
332 for host in it.filter("*", pairs(configmanager.getconfig())) do 337 for host in it.filter("*", pairs(configmanager.getconfig())) do
333 if api(host):get_option_boolean("enabled") == false then 338 if api(host):get_option_boolean("enabled") == false then
800 print(" WILL NOT migrate any existing data (user accounts, etc.) to the new name."); 805 print(" WILL NOT migrate any existing data (user accounts, etc.) to the new name.");
801 ok = false; 806 ok = false;
802 end 807 end
803 end 808 end
804 809
810 -- Check features
811 do
812 local missing_features = {};
813 for host in enabled_user_hosts() do
814 local all_features = checks.features(host, true);
815 if not all_features then
816 table.insert(missing_features, host);
817 end
818 end
819 if #missing_features > 0 then
820 print("");
821 print(" Some of your hosts may be missing features due to a lack of configuration.");
822 print(" For more details, use the 'prosodyctl check features' command.");
823 end
824 end
825
805 print("Done.\n"); 826 print("Done.\n");
806 end 827 end
807 function checks.dns() 828 function checks.dns()
808 local dns = require "prosody.net.dns"; 829 local dns = require "prosody.net.dns";
809 pcall(function () 830 pcall(function ()
1448 else 1469 else
1449 print("Success!\n"); 1470 print("Success!\n");
1450 end 1471 end
1451 end 1472 end
1452 end 1473 end
1474
1475 function checks.features(host, quiet)
1476 if not quiet then
1477 print("Feature report");
1478 end
1479
1480 local common_subdomains = {
1481 http_file_share = "share";
1482 muc = "groups";
1483 };
1484
1485 local function print_feature_status(feature, host)
1486 if quiet then return; end
1487 print("", feature.ok and "OK" or "(!)", feature.name);
1488 if not feature.ok then
1489 if feature.lacking_modules then
1490 table.sort(feature.lacking_modules);
1491 print("", "", "Suggested modules: ");
1492 for _, module in ipairs(feature.lacking_modules) do
1493 print("", "", (" - %s: https://prosody.im/doc/modules/mod_%s"):format(module, module));
1494 end
1495 end
1496 if feature.lacking_components then
1497 table.sort(feature.lacking_components);
1498 for _, component_module in ipairs(feature.lacking_components) do
1499 local subdomain = common_subdomains[component_module];
1500 if subdomain then
1501 print("", "", "Suggested component:");
1502 print("");
1503 print("", "", "", ("Component %q %q"):format(subdomain.."."..host, component_module));
1504 print("", "", "", ("-- Documentation: https://prosody.im/doc/modules/mod_%s"):format(component_module));
1505 else
1506 print("", "", ("Suggested component: %s"):format(component_module));
1507 end
1508 end
1509 print("");
1510 print("", "", "If you have already configured any these components, they may not be");
1511 print("", "", "linked correctly to "..host..". For more info see https://prosody.im/doc/components");
1512 end
1513 end
1514 print("");
1515 end
1516
1517 local all_ok = true;
1518
1519 local config = configmanager.getconfig();
1520
1521 local f, s, v;
1522 if check_host then
1523 f, s, v = it.values({ check_host });
1524 else
1525 f, s, v = enabled_user_hosts();
1526 end
1527
1528 for host in f, s, v do
1529 local modules_enabled = set.new(config["*"].modules_enabled);
1530 modules_enabled:include(set.new(config[host].modules_enabled));
1531
1532 -- { [component_module] = { hostname1, hostname2, ... } }
1533 local host_components = setmetatable({}, { __index = function (t, k) return rawset(t, k, {})[k]; end });
1534
1535 do
1536 local hostapi = api(host);
1537
1538 -- Find implicitly linked components
1539 for other_host in enabled_components() do
1540 local parent_host = other_host:match("^[^.]+%.(.+)$");
1541 if parent_host == host then
1542 local component_module = configmanager.get(other_host, "component_module");
1543 if component_module then
1544 table.insert(host_components[component_module], other_host);
1545 end
1546 end
1547 end
1548
1549 -- And components linked explicitly
1550 for _, disco_item in ipairs(hostapi:get_option_array("disco_items", {})) do
1551 local other_host = disco_item[1];
1552 local component_module = configmanager.get(other_host, "component_module");
1553 if component_module then
1554 table.insert(host_components[component_module], other_host);
1555 end
1556 end
1557 end
1558
1559 local current_feature;
1560
1561 local function check_module(suggested, alternate, ...)
1562 if set.intersection(modules_enabled, set.new({suggested, alternate, ...})):empty() then
1563 current_feature.lacking_modules = current_feature.lacking_modules or {};
1564 table.insert(current_feature.lacking_modules, suggested);
1565 end
1566 end
1567
1568 local function check_component(suggested, alternate, ...)
1569 local found;
1570 for _, component_module in ipairs({ suggested, alternate, ... }) do
1571 found = #host_components[component_module] > 0;
1572 if found then break; end
1573 end
1574 if not found then
1575 current_feature.lacking_components = current_feature.lacking_components or {};
1576 table.insert(current_feature.lacking_components, suggested);
1577 end
1578 end
1579
1580 local features = {
1581 {
1582 name = "Basic functionality";
1583 check = function ()
1584 check_module("disco");
1585 check_module("roster");
1586 check_module("saslauth");
1587 check_module("tls");
1588 check_module("pep");
1589 end;
1590 };
1591 {
1592 name = "Multi-device sync";
1593 check = function ()
1594 check_module("carbons");
1595 check_module("mam");
1596 check_module("bookmarks");
1597 end;
1598 };
1599 {
1600 name = "Mobile optimizations";
1601 check = function ()
1602 check_module("smacks");
1603 check_module("csi_simple", "csi_battery_saver");
1604 end;
1605 };
1606 {
1607 name = "Web connections";
1608 check = function ()
1609 check_module("bosh");
1610 check_module("websocket");
1611 end;
1612 };
1613 {
1614 name = "User profiles";
1615 check = function ()
1616 check_module("vcard_legacy", "vcard");
1617 end;
1618 };
1619 {
1620 name = "Blocking";
1621 check = function ()
1622 check_module("blocklist");
1623 end;
1624 };
1625 {
1626 name = "Push notifications";
1627 check = function ()
1628 check_module("cloud_notify");
1629 end;
1630 };
1631 {
1632 name = "Audio/video calls";
1633 check = function ()
1634 check_module(
1635 "turn_external",
1636 "external_services",
1637 "turncredentials",
1638 "extdisco"
1639 );
1640 end;
1641 };
1642 {
1643 name = "File sharing";
1644 check = function ()
1645 check_component("http_file_share", "http_upload");
1646 end;
1647 };
1648 {
1649 name = "Group chats";
1650 check = function ()
1651 check_component("muc");
1652 end;
1653 };
1654 };
1655
1656 if not quiet then
1657 print(host);
1658 end
1659
1660 for _, feature in ipairs(features) do
1661 current_feature = feature;
1662 feature.check();
1663 feature.ok = not feature.lacking_modules and not feature.lacking_components;
1664 -- For improved presentation, we group the (ok) and (not ok) features
1665 if feature.ok then
1666 print_feature_status(feature, host);
1667 end
1668 end
1669
1670 for _, feature in ipairs(features) do
1671 if not feature.ok then
1672 all_ok = false;
1673 print_feature_status(feature, host);
1674 end
1675 end
1676
1677 if not quiet then
1678 print("");
1679 end
1680 end
1681
1682 return all_ok;
1683 end
1684
1453 if what == nil or what == "all" then 1685 if what == nil or what == "all" then
1454 local ret; 1686 local ret;
1455 ret = checks.disabled(); 1687 ret = checks.disabled();
1456 if ret ~= nil then return ret; end 1688 if ret ~= nil then return ret; end
1457 ret = checks.config(); 1689 ret = checks.config();