indico: wip: create module

Signed-off-by: Jeltz <jeltz@federez.net>
This commit is contained in:
jeltz 2025-02-22 10:58:08 +01:00
parent d75eba0b8e
commit abbafb082d
Signed by: jeltz
GPG key ID: 800882B66C0C3326
2 changed files with 443 additions and 107 deletions

423
modules/indico.nix Normal file
View file

@ -0,0 +1,423 @@
# Inspired by https://git.hubrecht.ovh/hubrecht/nix-infra
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.indico;
pythonFmt = pkgs.formats.pythonVars { };
yamlFmt = pkgs.formats.yaml {};
indico = pkgs.callPackage ../pkgs/indico { };
pythonEnv = pkgs.python3.withPackages (ps: [
(ps.toPythonModule indico)
ps.gunicorn
]);
redisSocket = config.services.redis.servers.${cfg.redis.name}.unixSocket;
indicoSocket = "/run/indico/indico.sock";
baseDir = "${pythonEnv}/${pythonEnv.sitePackages}/indico";
loggingFile = yamlFmt.generate "logging.yaml" {
version = 1;
root = {
level = "INFO";
handlers = [ "stderr" ];
};
loggers = {
indico.handlers = [ "stderr" ];
celery.handlers = [ "stderr" ];
};
handlers.stderr = {
class = "logging.StreamHandler";
formatter = "default";
};
formatters.default = {
format = "%(levelname)s %(request_id)s %(user_id)s %(name)s %(message)s";
};
};
configFile = pythonFmt.generate "indico.conf" {
SQLALCHEMY_DATABASE_URI = cfg.database;
CACHE_DIR = "${cfg.stateDir}/cache";
TEMP_DIR = "${cfg.stateDir}/tmp";
LOG_DIR = "/dev/null";
STATIC_FILE_METHOD = [
"xaccelredirect"
{ ${cfg.stateDir} = "/.xsf/indico"; }
];
STORAGE_BACKENDS = {
default = "fs:${cfg.storageDir}";
};
DEFAULT_LOCALE = cfg.defaultLocale;
DEFAULT_TIMEZONE = cfg.defaultTimezone;
REDIS_CACHE_URL = cfg.cacheRedis;
CELERY_BROKER = cfg.celeryBrokerRedis;
USE_PROXY = true;
BASE_URL = cfg.baseUrl;
SECRET_KEY = cfg.secretKey;
LOGGING_CONFIG_FILE = loggingFile;
NO_REPLY_EMAIL = cfg.email.noReply;
SUPPORT_EMAIL = cfg.email.support;
SMTP_SENDER_FALLBACK = cfg.email.senderFallback;
PUBLIC_SUPPORT_EMAIL = cfg.email.publicSupport;
SMTP_SERVER = [ cfg.email.smtp.host cfg.email.smtp.port ];
SMTP_LOGIN = cfg.email.smtp.login;
SMTP_PASSWORD = cfg.email.smtp.password;
SMTP_USE_TLS = cfg.email.smtp.useTLS;
};
in
{
# TODO cProfile; indico standalone command is *very* slow
# (~30s just to print the help)
options.services.indico = {
enable = lib.mkEnableOption "indico";
# TODO use
package = lib.mkPackageOption pkgs "indico" { };
user = lib.mkOption {
type = lib.types.str;
default = "indico";
description = "User under which indico should run.";
};
group = lib.mkOption {
type = lib.types.str;
default = "indico";
description = "Group under which indico should run.";
};
email = {
noReply = lib.mkOption {
type = lib.types.str;
description = "No-reply email address.";
};
senderFallback = lib.mkOption {
type = lib.types.str;
default = cfg.email.noReply;
description = "Sender fallback email address.";
};
publicSupport = lib.mkOption {
type = lib.types.str;
description = "Public support email address.";
};
support = lib.mkOption {
type = lib.types.str;
description = "Support email address.";
};
smtp = {
host = lib.mkOption {
type = lib.types.str;
description = "SMTP host.";
};
port = lib.mkOption {
type = lib.types.port;
default = 587;
description = "SMTP port.";
};
useTLS = lib.mkOption {
type = lib.types.bool;
default = true;
description = "SMTP use TLS.";
};
login = lib.mkOption {
type = lib.types.str;
description = "SMTP login.";
};
password = lib.mkOption {
type = lib.types.str;
description = "SMTP password.";
};
};
};
secretKey = lib.mkOption {
type = lib.types.str;
description = "Secret key.";
};
baseUrl = lib.mkOption {
type = lib.types.str;
default = lib.optionalString
cfg.nginx.enable
"https://${cfg.nginx.domain}";
description = "Base URL.";
};
defaultLocale = lib.mkOption {
type = lib.types.str;
default = "fr_FR";
description = "Default locale.";
};
defaultTimezone = lib.mkOption {
type = lib.types.str;
default = "Europe/Paris";
description = "Default timezone.";
};
nginx = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to setup an NGinx virtual host.";
};
domain = lib.mkOption {
type = lib.types.str;
description = "NGinx virtual host domain.";
};
};
redis = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to setup a Redis server.";
};
name = lib.mkOption {
type = lib.types.str;
default = "indico";
description = "Redis server name.";
};
};
cacheRedis = lib.mkOption {
type = lib.types.str;
default = lib.optionalString cfg.redis.enable
"unix://${redisSocket}?db=0";
description = "Redis cache URL.";
};
celeryBrokerRedis = lib.mkOption {
type = lib.types.str;
default = lib.optionalString cfg.redis.enable
"redis+socket://${redisSocket}?virtual_host=1";
description = "Redis Celery broker URL.";
};
stateDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/indico";
description = ''
State directory (used for cache and temporary files).
'';
};
storageDir = lib.mkOption {
type = lib.types.path;
default = "${cfg.stateDir}/storage";
description = "Storage directory.";
};
postgresql = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to setup a PostgreSQL server.";
};
};
database = lib.mkOption {
type = lib.types.str;
default = lib.optionalString
cfg.postgresql.enable
"postgresql://@/${cfg.user}?host=/run/postgresql";
description = "Database URL.";
};
};
config = lib.mkIf cfg.enable {
users.users = {
${cfg.user} = {
isSystemUser = true;
group = cfg.group;
};
} // (lib.optionalAttrs cfg.nginx.enable {
${config.services.nginx.user}.extraGroups = [ cfg.group ];
});
users.groups.${cfg.group} = { };
services.postgresql = lib.mkIf cfg.postgresql.enable {
enable = true;
package = pkgs.postgresql_16; # TODO add setting
# TODO db name != cfg.user
ensureDatabases = [ cfg.user ];
ensureUsers = [
{
name = cfg.user;
ensureDBOwnership = true;
}
];
};
environment.systemPackages =
let
indico = pkgs.writeScriptBin "indico" ''
#!${pkgs.runtimeShell}
sudo=exec
if [[ "$USER" != "indico" ]]; then
sudo='exec /run/wrappers/bin/sudo -u indico -E INDICO_CONFIG'
fi
export INDICO_CONFIG=${configFile}
$sudo ${lib.getExe' pythonEnv "indico"} "$@"
'';
in
[ indico ];
services.redis = lib.mkIf cfg.redis.enable {
servers.${cfg.redis.name} = {
enable = true;
group = cfg.group;
};
};
systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/cache' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.stateDir}/tmp' 0750 ${cfg.user} ${cfg.group} - -"
"d '${cfg.storageDir}' 0750 ${cfg.user} ${cfg.group} - -"
];
systemd.services =
let
psqlExtensionsCommands = ''
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
'';
# TODO StateDirectory, CacheDirectory?
common = {
environment.INDICO_CONFIG = configFile;
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Group = cfg.group;
User = cfg.user;
# Restart = "on-failure";
RuntimeDirectory = "indico";
};
};
in
{
indico-db = lib.recursiveUpdate common {
description = "Indico database preparation and upgrade";
after = [
"postgresql.service"
];
serviceConfig.Type = "oneshot";
# Source: pretalx module ; passer par un service oneshot
script = ''
versionFile="${cfg.stateDir}/version"
if [[ ! -f "$versionFile" ]]; then
${lib.getExe' config.services.postgresql.package "psql"} \
-d "${cfg.database}" \
-c "${psqlExtensionsCommands}"
${lib.getExe' pythonEnv "indico"} db prepare
echo "${indico.version}" > "$versionFile"
fi
version="$(cat "$versionFile" 2>/dev/null || echo 0)"
# FIXME: not tested; may not work
if [[ "$version" != "${indico.version}" ]]; then
${lib.getExe' pythonEnv "indico"} db upgrade
echo "${indico.version}" > "$versionFile"
fi
'';
};
indico-worker = lib.recursiveUpdate common {
description = "Indico background worker";
after = [
"network.target"
"redis-indico.service"
"postgresql.service"
"indico-db.service"
];
serviceConfig.ExecStart = ''
${lib.getExe' pythonEnv "indico"} celery worker -B
'';
};
indico-web = lib.recursiveUpdate common {
description = "Indico web service";
after = [
"network.target"
"redis-indico.service"
"postgresql.service"
"indico-worker.service"
"indico-db.service"
];
# TODO bind on a TCP socket when cfg.nginx.enable == false?
serviceConfig.ExecStart = ''
${lib.getExe' pythonEnv "gunicorn"} \
--bind unix:${indicoSocket} \
--name=indico \
indico.web.wsgi
'';
};
};
systemd.sockets = lib.mkIf cfg.nginx.enable {
indico-web.socketConfig = {
ListenStream = indicoSocket;
SocketUser = config.services.nginx.user;
};
};
services.nginx = lib.mkIf cfg.nginx.enable {
enable = true;
recommendedProxySettings = lib.mkDefault true;
recommendedOptimisation = lib.mkDefault true;
recommendedTlsSettings = lib.mkDefault true;
recommendedGzipSettings = lib.mkDefault true;
upstreams.indico.servers."unix:${indicoSocket}" = { };
virtualHosts.${cfg.nginx.domain} = {
enableACME = true;
forceSSL = true;
locations = {
"/.xsf/indico/" = {
alias = "${cfg.stateDir}/";
extraConfig = ''
internal;
'';
};
"~ ^/(images|fonts)(.*)/(.+?)(__v[0-9a-f]+)?\\.([^.]+)$" = {
alias = "${baseDir}/web/static/$1$2/$3.$5";
priority = 900;
extraConfig = ''
access_log off;
'';
};
"~ ^/(css|dist|images|fonts)/(.*)$" = {
alias = "${baseDir}/web/static/$1/$2";
extraConfig = ''
access_log off;
'';
};
"= /robots.txt" = {
alias = "${baseDir}/web/static/robots.txt";
extraConfig = ''
access_log off;
'';
};
"/" = {
proxyPass = "http://indico";
# FIXME is it useful?
extraConfig = ''
client_max_body_size 1G;
'';
};
};
};
};
};
}

View file

@ -1,116 +1,29 @@
{ config, lib, pkgs, python3, ... }:
let
# cfg = config.services.indico;
# pythonFmt = pkgs.formats.pythonVars { };
indico = pkgs.callPackage ../pkgs/indico { };
pythonEnv = indico.python.withPackages (ps: [
indico
# (ps.toPythonModule indico)
ps.gunicorn
]);
in
{
# TODO cProfile; indico is *very* slow (~30s just to print the help)
# + sudo indico
environment.systemPackages = [ indico ];
services.redis.servers.indico.enable = true;
systemd.services.indico-web = {
description = "Indico web service";
after = [
"network.target"
"redis-indico.service"
"postgresql.service"
config,
lib,
pkgs,
...
}:
{
imports = [
../modules/indico.nix
];
wantedBy = [ "multi-user.target" ];
# TODO migrations
serviceConfig = {
User = "indico";
Group = "indico";
ExecStart = "${lib.getExe' pythonEnv "gunicorn"} --bind unix:/run/indico/indico.sock --name=indico indico.wsgi";
Restart = "on-failure";
};
enable = true;
};
systemd.sockets.gunicorn-web = {
socketConfig = {
ListenStream = "/run/indico/indico.sock";
SocketUser = "nginx";
};
enable = true;
};
# preStart = ''
# echo "create extension if not exists pg_trgm" | runuser -u ${config.services.postgresql.superUser} -- ${config.services.postgresql.package}/bin/psql hydra
# ''
services.postgresql = {
enable = true;
package = pkgs.postgresql_16;
ensureDatabases = [ "indico" ];
ensureUsers = [
{
name = "indico";
ensureDBOwnership = true;
}
];
};
networking.firewall.allowedTCPPorts = [ 80 443 ];
services.nginx = let
indicoBaseDir = "/tmp";
in {
services.indico = {
enable = true;
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
recommendedProxySettings = true;
upstreams.indico.servers."unix:/run/indico/indico.sock" = { };
virtualHosts."events.federez.net" = {
enableACME = true;
forceSSL = true;
locations = {
"/.xsf/indico/" = {
alias = "${indicoBaseDir}/";
extraConfig = ''
internal;
'';
};
# Order? + too lax?
"~ ^/(images|fonts)(.*)/(.+?)(__v[0-9a-f]+)?\\.([^.]+)$" = {
alias = "${indicoBaseDir}/web/static/$1$2/$3.$5";
extraConfig = ''
access_log off;
'';
};
"~ ^/(css|dist|images|fonts)/(.*)$" = {
alias = "${indicoBaseDir}/web/static/$1/$2";
extraConfig = ''
access_log off;
'';
};
"= /robots.txt" = {
alias = "${indicoBaseDir}/web/static/robots.txt";
extraConfig = ''
access_log off;
'';
"/" = {
proxyPass = "http://indico";
extraConfig = ''
client_max_body_size 1G;
'';
nginx.domain = "events.federez.net";
email = {
noReply = "indico@federez.net";
support = "admin@federez.net";
publicSupport = "bureau@federez.net";
smtp = {
host = "dodecagon.federez.net";
login = "indico";
password = "xxx";
};
};
secretKey = "lQsViT9292sIkObP9ptQADGJ16bk58n7"; # FIXME: dev only
};
};
users.users.indico = {
isSystemUser = true;
group = "indico";
};
users.groups.indico = {};
}