Software /
code /
prosody
File
util/prosodyctl/cert.lua @ 11523:5f15ab7c6ae5
Statistics: Rewrite statistics backends to use OpenMetrics
The metric subsystem of Prosody has had some shortcomings from
the perspective of the current state-of-the-art in metric
observability.
The OpenMetrics standard [0] is a formalization of the data
model (and serialization format) of the well-known and
widely-used Prometheus [1] software stack.
The previous stats subsystem of Prosody did not map well to that
format (see e.g. [2] and [3]); the key reason is that it was
trying to do too much math on its own ([2]) while lacking
first-class support for "families" of metrics ([3]) and
structured metric metadata (despite the `extra` argument to
metrics, there was no standard way of representing common things
like "tags" or "labels").
Even though OpenMetrics has grown from the Prometheus world of
monitoring, it maps well to other popular monitoring stacks
such as:
- InfluxDB (labels can be mapped to tags and fields as necessary)
- Carbon/Graphite (labels can be attached to the metric name with
dot-separation)
- StatsD (see graphite when assuming that graphite is used as
backend, which is the default)
The util.statsd module has been ported to use the OpenMetrics
model as a proof of concept. An implementation which exposes
the util.statistics backend data as Prometheus metrics is
ready for publishing in prosody-modules (most likely as
mod_openmetrics_prometheus to avoid breaking existing 0.11
deployments).
At the same time, the previous measure()-based API had one major
advantage: It is really simple and easy to use without requiring
lots of knowledge about OpenMetrics or similar concepts. For that
reason as well as compatibility with existing code, it is preserved
and may even be extended in the future.
However, code relying on the `stats-updated` event as well as
`get_stats` from `statsmanager` will break because the data
model has changed completely; in case of `stats-updated`, the
code will simply not run (as the event was renamed in order
to avoid conflicts); the `get_stats` function has been removed
completely (so it will cause a traceback when it is attempted
to be used).
Note that the measure_*_event methods have been removed from
the module API. I was unable to find any uses or documentation
and thus deemed they should not be ported. Re-implementation is
possible when necessary.
[0]: https://openmetrics.io/
[1]: https://prometheus.io/
[2]: #959
[3]: #960
author | Jonas Schäfer <jonas@wielicki.name> |
---|---|
date | Sun, 18 Apr 2021 11:47:41 +0200 |
parent | 11203:d10f59ac7f74 |
child | 12104:29765ac7f72f |
line wrap: on
line source
local lfs = require "lfs"; local pctl = require "util.prosodyctl"; local hi = require "util.human.io"; local configmanager = require "core.configmanager"; local openssl; local cert_commands = {}; -- If a file already exists, ask if the user wants to use it or replace it -- Backups the old file if replaced local function use_existing(filename) local attrs = lfs.attributes(filename); if attrs then if hi.show_yesno(filename .. " exists, do you want to replace it? [y/n]") then local backup = filename..".bkp~"..os.date("%FT%T", attrs.change); os.rename(filename, backup); pctl.show_message("%s backed up to %s", filename, backup); else -- Use the existing file return true; end end end local have_pposix, pposix = pcall(require, "util.pposix"); local cert_basedir = prosody.paths.data == "." and "./certs" or prosody.paths.data; if have_pposix and pposix.getuid() == 0 then -- FIXME should be enough to check if this directory is writable local cert_dir = configmanager.get("*", "certificates") or "certs"; cert_basedir = configmanager.resolve_relative_path(prosody.paths.config, cert_dir); end function cert_commands.config(arg) if #arg >= 1 and arg[1] ~= "--help" then local conf_filename = cert_basedir .. "/" .. arg[1] .. ".cnf"; if use_existing(conf_filename) then return nil, conf_filename; end local distinguished_name; if arg[#arg]:find("^/") then distinguished_name = table.remove(arg); end local conf = openssl.config.new(); conf:from_prosody(prosody.hosts, configmanager, arg); if distinguished_name then local dn = {}; for k, v in distinguished_name:gmatch("/([^=/]+)=([^/]+)") do table.insert(dn, k); dn[k] = v; end conf.distinguished_name = dn; else pctl.show_message("Please provide details to include in the certificate config file."); pctl.show_message("Leave the field empty to use the default value or '.' to exclude the field.") for _, k in ipairs(openssl._DN_order) do local v = conf.distinguished_name[k]; if v then local nv = nil; if k == "commonName" then v = arg[1] elseif k == "emailAddress" then v = "xmpp@" .. arg[1]; elseif k == "countryName" then local tld = arg[1]:match"%.([a-z]+)$"; if tld and #tld == 2 and tld ~= "uk" then v = tld:upper(); end end nv = hi.show_prompt(("%s (%s):"):format(k, nv or v)); nv = (not nv or nv == "") and v or nv; if nv:find"[\192-\252][\128-\191]+" then conf.req.string_mask = "utf8only" end conf.distinguished_name[k] = nv ~= "." and nv or nil; end end end local conf_file, err = io.open(conf_filename, "w"); if not conf_file then pctl.show_warning("Could not open OpenSSL config file for writing"); pctl.show_warning(err); os.exit(1); end conf_file:write(conf:serialize()); conf_file:close(); print(""); pctl.show_message("Config written to %s", conf_filename); return nil, conf_filename; else pctl.show_usage("cert config HOSTNAME [HOSTNAME+]", "Builds a certificate config file covering the supplied hostname(s)") end end function cert_commands.key(arg) if #arg >= 1 and arg[1] ~= "--help" then local key_filename = cert_basedir .. "/" .. arg[1] .. ".key"; if use_existing(key_filename) then return nil, key_filename; end os.remove(key_filename); -- This file, if it exists is unlikely to have write permissions local key_size = tonumber(arg[2] or hi.show_prompt("Choose key size (2048):") or 2048); local old_umask = pposix.umask("0377"); if openssl.genrsa{out=key_filename, key_size} then os.execute(("chmod 400 '%s'"):format(key_filename)); pctl.show_message("Key written to %s", key_filename); pposix.umask(old_umask); return nil, key_filename; end pctl.show_message("There was a problem, see OpenSSL output"); else pctl.show_usage("cert key HOSTNAME <bits>", "Generates a RSA key named HOSTNAME.key\n " .."Prompts for a key size if none given") end end function cert_commands.request(arg) if #arg >= 1 and arg[1] ~= "--help" then local req_filename = cert_basedir .. "/" .. arg[1] .. ".req"; if use_existing(req_filename) then return nil, req_filename; end local _, key_filename = cert_commands.key({arg[1]}); local _, conf_filename = cert_commands.config(arg); if openssl.req{new=true, key=key_filename, utf8=true, sha256=true, config=conf_filename, out=req_filename} then pctl.show_message("Certificate request written to %s", req_filename); else pctl.show_message("There was a problem, see OpenSSL output"); end else pctl.show_usage("cert request HOSTNAME [HOSTNAME+]", "Generates a certificate request for the supplied hostname(s)") end end function cert_commands.generate(arg) if #arg >= 1 and arg[1] ~= "--help" then local cert_filename = cert_basedir .. "/" .. arg[1] .. ".crt"; if use_existing(cert_filename) then return nil, cert_filename; end local _, key_filename = cert_commands.key({arg[1]}); local _, conf_filename = cert_commands.config(arg); if key_filename and conf_filename and cert_filename and openssl.req{new=true, x509=true, nodes=true, key=key_filename, days=365, sha256=true, utf8=true, config=conf_filename, out=cert_filename} then pctl.show_message("Certificate written to %s", cert_filename); print(); else pctl.show_message("There was a problem, see OpenSSL output"); end else pctl.show_usage("cert generate HOSTNAME [HOSTNAME+]", "Generates a self-signed certificate for the current hostname(s)") end end local function sh_esc(s) return "'" .. s:gsub("'", "'\\''") .. "'"; end local function copy(from, to, umask, owner, group) local old_umask = umask and pposix.umask(umask); local attrs = lfs.attributes(to); if attrs then -- Move old file out of the way local backup = to..".bkp~"..os.date("%FT%T", attrs.change); os.rename(to, backup); end -- FIXME friendlier error handling, maybe move above backup back? local input = assert(io.open(from)); local output = assert(io.open(to, "w")); local data = input:read(2^11); while data and output:write(data) do data = input:read(2^11); end assert(input:close()); assert(output:close()); if not prosody.installed then -- FIXME this is possibly specific to GNU chown os.execute(("chown -c --reference=%s %s"):format(sh_esc(cert_basedir), sh_esc(to))); elseif owner and group then local ok = os.execute(("chown %s:%s %s"):format(sh_esc(owner), sh_esc(group), sh_esc(to))); assert(ok == true or ok == 0, "Failed to change ownership of "..to); end if old_umask then pposix.umask(old_umask); end return true; end function cert_commands.import(arg) local hostnames = {}; -- Move hostname arguments out of arg, the rest should be a list of paths while arg[1] and prosody.hosts[ arg[1] ] do table.insert(hostnames, table.remove(arg, 1)); end if hostnames[1] == nil then local domains = os.getenv"RENEWED_DOMAINS"; -- Set if invoked via certbot if domains then for host in domains:gmatch("%S+") do table.insert(hostnames, host); end else for host in pairs(prosody.hosts) do if host ~= "*" and configmanager.get(host, "enabled") ~= false then table.insert(hostnames, host); end end end end if not arg[1] or arg[1] == "--help" then -- Probably forgot the path pctl.show_usage("cert import [HOSTNAME+] /path/to/certs [/other/paths/]+", "Copies certificates to "..cert_basedir); return 1; end local owner, group; if pposix.getuid() == 0 then -- We need root to change ownership owner = configmanager.get("*", "prosody_user") or "prosody"; group = configmanager.get("*", "prosody_group") or owner; end local cm = require "core.certmanager"; local imported = {}; for _, host in ipairs(hostnames) do for _, dir in ipairs(arg) do local paths = cm.find_cert(dir, host); if paths then copy(paths.certificate, cert_basedir .. "/" .. host .. ".crt", nil, owner, group); copy(paths.key, cert_basedir .. "/" .. host .. ".key", "0377", owner, group); table.insert(imported, host); else -- TODO Say where we looked pctl.show_warning("No certificate for host "..host.." found :("); end -- TODO Additional checks -- Certificate names matches the hostname -- Private key matches public key in certificate end end if imported[1] then pctl.show_message("Imported certificate and key for hosts %s", table.concat(imported, ", ")); local ok, err = pctl.reload(); if not ok and err ~= "not-running" then pctl.show_message(pctl.error_messages[err]); end else pctl.show_warning("No certificates imported :("); return 1; end end local function cert(arg) if #arg >= 1 and arg[1] ~= "--help" then openssl = require "util.openssl"; lfs = require "lfs"; local cert_dir_attrs = lfs.attributes(cert_basedir); if not cert_dir_attrs then pctl.show_warning("The directory "..cert_basedir.." does not exist"); return 1; -- TODO Should we create it? end local uid = pposix.getuid(); if uid ~= 0 and uid ~= cert_dir_attrs.uid then pctl.show_warning("The directory "..cert_basedir.." is not owned by the current user, won't be able to write files to it"); return 1; elseif not cert_dir_attrs.permissions then -- COMPAT with LuaFilesystem < 1.6.2 (hey CentOS!) pctl.show_message("Unable to check permissions on %s (LuaFilesystem 1.6.2+ required)", cert_basedir); pctl.show_message("Please confirm that Prosody (and only Prosody) can write to this directory)"); elseif cert_dir_attrs.permissions:match("^%.w..%-..%-.$") then pctl.show_warning("The directory "..cert_basedir.." not only writable by its owner"); return 1; end local subcmd = table.remove(arg, 1); if type(cert_commands[subcmd]) == "function" then if subcmd ~= "import" then -- hostnames are optional for import if not arg[1] then pctl.show_message"You need to supply at least one hostname" arg = { "--help" }; end if arg[1] ~= "--help" and not prosody.hosts[arg[1]] then pctl.show_message(pctl.error_messages["no-such-host"]); return 1; end end return cert_commands[subcmd](arg); elseif subcmd == "check" then return require "util.prosodyctl.check".check({"certs"}); end end pctl.show_usage("cert config|request|generate|key|import", "Helpers for generating X.509 certificates and keys.") for _, cmd in pairs(cert_commands) do print() cmd{ "--help" } end end return { cert = cert; };